Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0658bb55a | ||
|
|
cf8c28c34b | ||
|
|
60dcd14390 | ||
|
|
8344e218a6 | ||
|
|
740b2d74df | ||
|
|
da0189e641 | ||
|
|
ad778a6f29 | ||
|
|
c35edd1ee8 | ||
|
|
bb8ad4406b | ||
|
|
111b48dcf4 | ||
|
|
6dccefe687 | ||
|
|
9ea950c9af | ||
|
|
7d100fff9f | ||
|
|
6b69c7f5ac | ||
|
|
691d402341 | ||
|
|
b96211a325 | ||
|
|
17b9ba61d9 | ||
|
|
f83b4e4e84 | ||
|
|
4de331597a | ||
|
|
6e295998ef | ||
|
|
f2dae095f8 | ||
|
|
6f5715fc30 | ||
|
|
1ece71a831 | ||
|
|
197351dd73 | ||
|
|
0d671dafea | ||
|
|
9ae7c39125 | ||
|
|
ba8fb4dff1 | ||
|
|
194661197e | ||
|
|
298acd1cdb | ||
|
|
97c608c346 | ||
|
|
f84bc19bf2 | ||
|
|
91375778cb | ||
|
|
38ddc98fef | ||
|
|
1b7c73273d | ||
|
|
5bd5e75c3b | ||
|
|
1cba584ac8 | ||
|
|
dfafea092e | ||
|
|
0ae4bfb222 | ||
|
|
7db4413476 | ||
|
|
4d52d18ef7 | ||
|
|
39ff86c551 | ||
|
|
12fdd8ea9b | ||
|
|
28f8eba9a2 | ||
|
|
b3b14202d9 | ||
|
|
ffa3ef1ccf | ||
|
|
d13f4155af | ||
|
|
d23e42e27f | ||
|
|
872efb180d | ||
|
|
2e61f32b6d | ||
|
|
7abb3bdddc | ||
|
|
67fd9824cc | ||
|
|
1878e30bdb | ||
|
|
88bbffd614 | ||
|
|
a50a2fd983 | ||
|
|
c950a57a3e | ||
|
|
80404da8f6 | ||
|
|
010d516b11 | ||
|
|
fb42733873 | ||
|
|
1070b2f434 | ||
|
|
9675e2285a | ||
|
|
a09ddbb0a6 | ||
|
|
81a12b7774 | ||
|
|
88ba7766e0 | ||
|
|
6475917b3f | ||
|
|
5b6e04cb4d | ||
|
|
2bfb14751f | ||
|
|
b1998960bb | ||
|
|
c4dcbf379f | ||
|
|
3b5bd39743 | ||
|
|
4e4cde1118 | ||
|
|
e1db6d8831 | ||
|
|
db8a1cdc7d | ||
|
|
d25aacb207 | ||
|
|
106539b5d2 | ||
|
|
d716bc2703 | ||
|
|
aa0dcb9a70 | ||
|
|
01366ef526 | ||
|
|
6145599638 | ||
|
|
abd4f54d78 | ||
|
|
adff7f6d34 | ||
|
|
2364f04991 | ||
|
|
46a5c54844 | ||
|
|
2caacae2e4 | ||
|
|
1ff5e0c88b | ||
|
|
c150876e5f | ||
|
|
758da1e311 | ||
|
|
250b48aa3b | ||
|
|
0a9ee79fbb | ||
|
|
58b8b93ee6 | ||
|
|
aef8673666 | ||
|
|
901ec18aa7 | ||
|
|
56b62b64cb | ||
|
|
abbf9061ea | ||
|
|
8f28bd9c3f | ||
|
|
68782004a7 | ||
|
|
0a146d10cc | ||
|
|
beaa33d088 | ||
|
|
2a94668e6e | ||
|
|
56e6494b36 | ||
|
|
033a177078 | ||
|
|
b9fffa3b41 | ||
|
|
7be85838fa | ||
|
|
37f8832229 | ||
|
|
7ebf5c3032 | ||
|
|
7d4d42097d | ||
|
|
33e1b570bf | ||
|
|
8920cc1432 | ||
|
|
9b480f985b | ||
|
|
daab3f80a0 | ||
|
|
ec8782a486 | ||
|
|
6745f775b2 | ||
|
|
7923e4f383 | ||
|
|
d280846843 | ||
|
|
178b654a1b | ||
|
|
3a516a3f2b | ||
|
|
d5316d558f | ||
|
|
a7216decad | ||
|
|
8d545610d2 | ||
|
|
23c27b629d | ||
|
|
0220d81513 | ||
|
|
aabe5dad76 | ||
|
|
741757f2a7 | ||
|
|
f5e543576e | ||
|
|
e6b641a075 | ||
|
|
3822609621 | ||
|
|
0f65cb065a | ||
|
|
ae19236e08 | ||
|
|
f422b77aaa | ||
|
|
43b3e94506 | ||
|
|
2d86cb0027 | ||
|
|
792d4cc631 | ||
|
|
f8016bca48 | ||
|
|
82818f7126 | ||
|
|
5c8f9aac64 | ||
|
|
1a8bad1bf2 | ||
|
|
4b795871b6 | ||
|
|
3036401c7c | ||
|
|
8bbba572de | ||
|
|
968b51319e | ||
|
|
900f25f832 | ||
|
|
bb1f1af113 | ||
|
|
a32ce8b252 | ||
|
|
331e565c64 | ||
|
|
0dc4d7d9f8 | ||
|
|
86eebc5b3f | ||
|
|
0bb98d431b | ||
|
|
6634773145 | ||
|
|
b5704cc7d0 | ||
|
|
8923da00e3 | ||
|
|
0e1a5d3cfe | ||
|
|
1b1bbf1ad9 | ||
|
|
dc79914182 | ||
|
|
b86b8ba47e | ||
|
|
e88377f517 | ||
|
|
9a3c732dde | ||
|
|
784f6eda93 | ||
|
|
35619fd892 | ||
|
|
7cfe015564 | ||
|
|
62759e4bac | ||
|
|
08cc3015b5 | ||
|
|
3781a4dc3a | ||
|
|
08f315016f | ||
|
|
cbf4b8c85d | ||
|
|
0a7a41cc2c | ||
|
|
af9c0aaeb6 | ||
|
|
af1e2b89f4 | ||
|
|
9581ba791a | ||
|
|
c35d002dc2 | ||
|
|
09946ee776 | ||
|
|
e94ff76527 | ||
|
|
92d46d0069 | ||
|
|
4b85aa2cec | ||
|
|
d332f262e7 | ||
|
|
2f9061e8e0 | ||
|
|
f2d2522721 | ||
|
|
5fa100b997 | ||
|
|
ddf00f779c | ||
|
|
69255ecbc8 | ||
|
|
ec4dafae0c | ||
|
|
ddb2434357 | ||
|
|
da3e093a1d | ||
|
|
8a0961c9dc | ||
|
|
88ade1d312 | ||
|
|
87b5ee077d | ||
|
|
d7013b19c1 | ||
|
|
bd5f02ad8d | ||
|
|
03605cc3ed | ||
|
|
218d2e1a09 | ||
|
|
3e47910f39 | ||
|
|
b09758bcfe | ||
|
|
c7d2c37565 | ||
|
|
388ce16d86 | ||
|
|
8c24718090 | ||
|
|
44d3faad45 | ||
|
|
ee7f0c22b8 | ||
|
|
a2bd1f29e2 | ||
|
|
a029cd22bd | ||
|
|
d732ee4851 | ||
|
|
320477cf5e | ||
|
|
a7adda21fd | ||
|
|
4971af9fc9 | ||
|
|
a0b42e8cc1 | ||
|
|
47d706734c | ||
|
|
a4c700b23b | ||
|
|
e3d5bb718e | ||
|
|
1f4f0aba3b | ||
|
|
b710c8822c | ||
|
|
4d69bd7059 | ||
|
|
a19140a479 | ||
|
|
e1edece58c | ||
|
|
f1d7f09648 | ||
|
|
0886847c4c | ||
|
|
d1a218b893 | ||
|
|
1bbcf67366 | ||
|
|
8f7416d9d9 | ||
|
|
f9cca8fd13 | ||
|
|
61b5aafd24 | ||
|
|
7410cd370a | ||
|
|
e4431b8589 | ||
|
|
78c9666ea1 | ||
|
|
60efbf751d | ||
|
|
3afe4d6dfc | ||
|
|
186d1ab201 | ||
|
|
8033c90778 | ||
|
|
c2abb107d6 | ||
|
|
3ed3bf9108 | ||
|
|
883631f5a6 | ||
|
|
d89a2a448e | ||
|
|
bd6ada3be8 | ||
|
|
dc5f8de3f9 | ||
|
|
0ba36abc97 | ||
|
|
f23b28374b | ||
|
|
bc3c915563 | ||
|
|
fd7ea5beb0 | ||
|
|
e23c34db97 | ||
|
|
c7bd0fe7e7 | ||
|
|
6935f747e3 | ||
|
|
9a74a73a00 | ||
|
|
af915cfc20 | ||
|
|
3f6c748b1f | ||
|
|
6ee16616c0 | ||
|
|
55fbc528f9 | ||
|
|
7de603134c | ||
|
|
d35d60f4c9 | ||
|
|
9c0fa3cfef | ||
|
|
1ecdd2d736 | ||
|
|
d1014500e8 | ||
|
|
4e96f92d1e | ||
|
|
6c1138d6f5 | ||
|
|
a6b68dd684 | ||
|
|
e66e6efb13 | ||
|
|
6510f25e30 | ||
|
|
f632fc648e | ||
|
|
5225b33bab | ||
|
|
cb9de080e5 | ||
|
|
77abd0606f | ||
|
|
df114285ce | ||
|
|
32690dd7d1 | ||
|
|
10077bdb5e | ||
|
|
abbc3924be | ||
|
|
19451c72e9 | ||
|
|
f017e109f5 | ||
|
|
f1594048a5 | ||
|
|
ce6c16016c | ||
|
|
8e6c898fca | ||
|
|
7c277d083c | ||
|
|
3111a5b78a | ||
|
|
646577af50 | ||
|
|
9a3d1a0321 | ||
|
|
d87aadf8bd | ||
|
|
a4956eee40 | ||
|
|
e56f4c8435 | ||
|
|
8ed7b7b5ac | ||
|
|
0bd8ad7dc2 | ||
|
|
08e02cff84 | ||
|
|
c6250bc57b | ||
|
|
c8c4ec5155 | ||
|
|
c365eac5dc | ||
|
|
afa16a69d5 | ||
|
|
0349240827 | ||
|
|
d46fa4b042 | ||
|
|
34010cf418 | ||
|
|
6d82f1c1e6 | ||
|
|
8f9265b7f3 | ||
|
|
0408c97915 | ||
|
|
4774c2759a | ||
|
|
0d139c15df | ||
|
|
f5a9a65c42 | ||
|
|
29b29c5fbf | ||
|
|
1bc8bc5e76 | ||
|
|
91d0b9f65c | ||
|
|
e9d548b122 | ||
|
|
30c2eae6ae | ||
|
|
6d31f098cc | ||
|
|
bdf66e7d6e | ||
|
|
ce79c2c83d | ||
|
|
7e9006988d | ||
|
|
a6e310a41a | ||
|
|
92052fbe5a | ||
|
|
3817c18619 | ||
|
|
eb97ac6a2c | ||
|
|
8dfc35383d | ||
|
|
22dcba75d2 | ||
|
|
bff7f463a1 | ||
|
|
5eae77f8c4 | ||
|
|
d34b708d53 | ||
|
|
9321f2f20b | ||
|
|
518a7f9694 | ||
|
|
2389400354 | ||
|
|
bd454e286d | ||
|
|
9f984f82d2 | ||
|
|
2bdd84b37b | ||
|
|
b50c5eb031 | ||
|
|
96a41c81c5 | ||
|
|
9f977be589 | ||
|
|
03ef7a1059 | ||
|
|
51c2cbff03 | ||
|
|
a406527510 | ||
|
|
38a7c77786 | ||
|
|
682b6d5b21 | ||
|
|
3b92c0d5a2 | ||
|
|
fddb26d43e | ||
|
|
47d3a18615 | ||
|
|
997d92575b | ||
|
|
7e273b8443 | ||
|
|
379f38e64e | ||
|
|
9b9ce35894 | ||
|
|
26d9b44478 | ||
|
|
3f930b4a92 | ||
|
|
f94dac17c6 | ||
|
|
b87b0a6e86 | ||
|
|
6b1208b035 | ||
|
|
d538a16479 | ||
|
|
a720c37c20 | ||
|
|
c0f5129c3e | ||
|
|
cb3725234b | ||
|
|
4e76929e59 | ||
|
|
0cb4b4b9ad | ||
|
|
22856a95bf | ||
|
|
900cdd0f1f | ||
|
|
e505b3aa28 | ||
|
|
6151e30874 | ||
|
|
625d096517 | ||
|
|
f7e33c70c8 | ||
|
|
6a72d2926c | ||
|
|
a94700f861 | ||
|
|
93dc98946b | ||
|
|
43681f670d | ||
|
|
44cce79e00 | ||
|
|
0099c2aec5 | ||
|
|
e0d04e1ffe | ||
|
|
0c2b763d02 | ||
|
|
52158d7de3 | ||
|
|
ec2733611b | ||
|
|
fc8793d96a | ||
|
|
63fc8b6b9e | ||
|
|
17b8b364c0 | ||
|
|
d5931e4e27 | ||
|
|
415bc8714b | ||
|
|
fb69ea792e | ||
|
|
1e709961e1 | ||
|
|
ae24200ded | ||
|
|
001a64e2a9 | ||
|
|
483a1b9f7e | ||
|
|
284f0b2501 | ||
|
|
820ce069a8 | ||
|
|
88ad5d2c2b | ||
|
|
fb76eb0b84 | ||
|
|
458fcaf50f | ||
|
|
37bcb194df | ||
|
|
4a39114f06 | ||
|
|
76a92b5a80 | ||
|
|
dced276114 | ||
|
|
f2fe43e85d | ||
|
|
6a395a7da1 | ||
|
|
11b2e3e333 | ||
|
|
33be0abbf4 | ||
|
|
717efbeaa9 | ||
|
|
ea8f6c8c6b | ||
|
|
146fb4e3ff | ||
|
|
c4b5974d23 | ||
|
|
cb8abb5ba4 | ||
|
|
ba4c160080 | ||
|
|
a6439f42ef | ||
|
|
0b0da7db51 | ||
|
|
450aacb88f | ||
|
|
a9f650a9e9 | ||
|
|
cf37d0a676 | ||
|
|
73017e18fd | ||
|
|
d459dd5a4f | ||
|
|
d3a6395edf | ||
|
|
ec24260ffc | ||
|
|
ed02fa6e3c | ||
|
|
5aec96818b | ||
|
|
a1112fe895 | ||
|
|
bc45fabd9c | ||
|
|
feb7dc5b0c | ||
|
|
998504005b | ||
|
|
9decffaf82 | ||
|
|
27fabde683 | ||
|
|
48b471d055 | ||
|
|
c6f55001b5 | ||
|
|
41a21175d2 | ||
|
|
4614de646a | ||
|
|
3116e6292a | ||
|
|
7c1cc52c86 | ||
|
|
3e42b9599c | ||
|
|
f53eaefa87 | ||
|
|
097de75b2b | ||
|
|
3bee1f2f53 | ||
|
|
d772c30099 | ||
|
|
2904713b04 | ||
|
|
061d9887c7 | ||
|
|
63d6834524 | ||
|
|
056042bc3e | ||
|
|
675d001e62 | ||
|
|
f027c66712 | ||
|
|
b2b55d534e | ||
|
|
234906de8f | ||
|
|
47c3390232 | ||
|
|
ed972c66ae | ||
|
|
544ff7bb02 | ||
|
|
ff4344e973 | ||
|
|
7858bac32b | ||
|
|
5a5947c2d9 | ||
|
|
01d11af42c | ||
|
|
d34e68e1b0 | ||
|
|
c0ea321446 | ||
|
|
a2a6379034 | ||
|
|
61e8dee760 | ||
|
|
23793ee669 | ||
|
|
43b423c61c | ||
|
|
289f4e1da8 | ||
|
|
32cd48b398 | ||
|
|
15ce8706e2 | ||
|
|
c1f8bae16a | ||
|
|
dd295f4bc7 | ||
|
|
51a9977b79 | ||
|
|
2f25dc2a51 | ||
|
|
2d2ec036f8 | ||
|
|
41a18e9e23 | ||
|
|
24efbdfb7c | ||
|
|
36e71050cb | ||
|
|
e00524ba5b | ||
|
|
4411dbe3f2 | ||
|
|
93ff4a22de | ||
|
|
2ee7b59921 | ||
|
|
16d8d1af28 | ||
|
|
9edde54c3b | ||
|
|
e374882c03 | ||
|
|
999ecf73de | ||
|
|
1cd7e53875 | ||
|
|
e55472e9b8 | ||
|
|
f7fcc160b8 | ||
|
|
365cd44b1c | ||
|
|
3a706f7a1a | ||
|
|
e96ca906a4 | ||
|
|
58bb5194d3 | ||
|
|
1b33e7259e | ||
|
|
d82adfad71 | ||
|
|
3c0efc1e2a | ||
|
|
970691db2d | ||
|
|
5547e35ffb | ||
|
|
e9e6f23105 | ||
|
|
2a97e7e0fb | ||
|
|
e8409fe8fb | ||
|
|
4a9c11def4 | ||
|
|
a446fcc95c | ||
|
|
8e31412bae | ||
|
|
9bdbcbb7f9 | ||
|
|
189dbcec67 | ||
|
|
e785d86095 | ||
|
|
d0d80aee53 | ||
|
|
e27dd766ba | ||
|
|
0d76145e1b | ||
|
|
51830c13a1 | ||
|
|
4895540a32 | ||
|
|
0843b401b0 | ||
|
|
d8d2434c49 | ||
|
|
2ab9047d12 | ||
|
|
9382c83d33 | ||
|
|
6909684d6b | ||
|
|
56098b98db | ||
|
|
d4cbba0416 | ||
|
|
797789338e | ||
|
|
8074811400 | ||
|
|
1fcdc52854 | ||
|
|
f60d54bbc1 | ||
|
|
cd5f45b1c6 | ||
|
|
8bc67d384b | ||
|
|
ecb3d572f7 | ||
|
|
2a0b83af2f | ||
|
|
67207f4b75 | ||
|
|
9b74eceb79 | ||
|
|
0700c8b780 | ||
|
|
694b947508 | ||
|
|
7795adcd60 | ||
|
|
a65dae971f | ||
|
|
2c78c731f7 | ||
|
|
99785cc16b | ||
|
|
70bbfd0964 | ||
|
|
8cbea343ee | ||
|
|
ad5f5c6e37 | ||
|
|
8fc96d6246 | ||
|
|
587a0a9b61 | ||
|
|
11cfb574c7 | ||
|
|
028dbd71a2 | ||
|
|
fe95fd035b | ||
|
|
c8827b6cab | ||
|
|
2f506d976d | ||
|
|
c312b47d08 | ||
|
|
96996cef7d | ||
|
|
0a66246e97 | ||
|
|
79c90bf8eb | ||
|
|
384ca45a16 | ||
|
|
7b67dd4553 | ||
|
|
2e87bc8385 | ||
|
|
88d6436a39 | ||
|
|
663ea2e39b | ||
|
|
ce9508639b | ||
|
|
86813b4ba6 | ||
|
|
774d10b83e | ||
|
|
d773207335 | ||
|
|
fe92444873 | ||
|
|
4d008142d7 | ||
|
|
1a04a8216d | ||
|
|
f3c4c8cb4d | ||
|
|
cde6d02e34 | ||
|
|
a7edd6ea47 | ||
|
|
73b65eca5a | ||
|
|
acca527b05 | ||
|
|
7303e58d2b | ||
|
|
1459d23925 | ||
|
|
a6661e4557 | ||
|
|
72044e8ef7 | ||
|
|
a9e826e3bb | ||
|
|
4cd08e5f65 | ||
|
|
e23b60b37f | ||
|
|
6620b48fd1 | ||
|
|
d80a567532 | ||
|
|
f01df3dcb7 | ||
|
|
be4a62aa7e | ||
|
|
a9b0396f0c | ||
|
|
b02808dd21 | ||
|
|
3f673136bd | ||
|
|
79dd9c4af6 | ||
|
|
698fcdb82c | ||
|
|
89ff2a798c | ||
|
|
9b132a2e6d | ||
|
|
07ff933f78 | ||
|
|
befe07559a | ||
|
|
47a5e30155 | ||
|
|
fcc743c47f | ||
|
|
0da6662b5e | ||
|
|
7b3558d030 | ||
|
|
0cdd845004 | ||
|
|
b7d3a29db8 | ||
|
|
44c18a3894 | ||
|
|
cba29235ab | ||
|
|
2f4a3138cd | ||
|
|
666c26a789 | ||
|
|
858dd1706b | ||
|
|
24883a476c | ||
|
|
9977c7d05a | ||
|
|
04b6cd342c | ||
|
|
e2d09bcb6d | ||
|
|
9041fca941 | ||
|
|
cacf83a98d | ||
|
|
8f43be2ca6 | ||
|
|
14f24b0d52 | ||
|
|
65fb415b11 | ||
|
|
79c89c84ae | ||
|
|
ec0d3d7b19 | ||
|
|
bacea2cef6 | ||
|
|
97873b87c9 | ||
|
|
2f6e763ee5 | ||
|
|
486eb46795 | ||
|
|
1f79973768 | ||
|
|
ee3263e02f | ||
|
|
3509fd9631 | ||
|
|
34f54c0cea | ||
|
|
06ed2ae8b2 | ||
|
|
7711aa1919 | ||
|
|
0872ee5444 | ||
|
|
a6c83879db | ||
|
|
3521675e75 | ||
|
|
e1beb778c4 | ||
|
|
9eb39bfe58 | ||
|
|
e624c9c25a | ||
|
|
9967cbc49e | ||
|
|
3930858c3b | ||
|
|
ba04d2e0cf | ||
|
|
d7c6edbe78 | ||
|
|
edc2326130 |
@@ -32,7 +32,7 @@ jobs:
|
||||
build_docs: build_docs
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Get tags
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: CI for Markdown content
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'lib/**/*.md'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'lib/**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Markdown content
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Run markdownlint
|
||||
uses: DavidAnson/markdownlint-cli2-action@v13.0.0
|
||||
with:
|
||||
globs: |
|
||||
lib/elixir/pages/**/*.md
|
||||
+11
-11
@@ -3,10 +3,10 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'lib/**/*.md'
|
||||
- "lib/**/*.md"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'lib/**/*.md'
|
||||
- "lib/**/*.md"
|
||||
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
@@ -24,19 +24,19 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_version: '26.0'
|
||||
- otp_version: "26.0"
|
||||
otp_latest: true
|
||||
- otp_version: '25.3'
|
||||
- otp_version: '25.0'
|
||||
- otp_version: '24.3'
|
||||
- otp_version: '24.0'
|
||||
- otp_version: "25.3"
|
||||
- otp_version: "25.0"
|
||||
- otp_version: "24.3"
|
||||
- otp_version: "24.0"
|
||||
- otp_version: master
|
||||
development: true
|
||||
- otp_version: maint
|
||||
development: true
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@v1
|
||||
@@ -77,12 +77,12 @@ jobs:
|
||||
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
|
||||
strategy:
|
||||
matrix:
|
||||
otp_version: ['24', '25', '26']
|
||||
otp_version: ["24", "25", "26.0"]
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@v1
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Shellcheck
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
name: Notify
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@v1
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
build_docs: build_docs
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: ./.github/workflows/release_pre_built
|
||||
|
||||
@@ -36,17 +36,21 @@ runs:
|
||||
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
|
||||
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
|
||||
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
|
||||
- name: Get latest stable ExDoc version
|
||||
- name: Get ExDoc ref
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
EX_DOC_LATEST_STABLE_VERSION=$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
echo "EX_DOC_LATEST_STABLE_VERSION=${EX_DOC_LATEST_STABLE_VERSION}" >> $GITHUB_ENV
|
||||
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||
ref=main
|
||||
else
|
||||
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
fi
|
||||
echo "EX_DOC_REF=$ref" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
if: ${{ inputs.build_docs }}
|
||||
with:
|
||||
repository: elixir-lang/ex_doc
|
||||
ref: v${{ env.EX_DOC_LATEST_STABLE_VERSION }}
|
||||
ref: ${{ env.EX_DOC_REF }}
|
||||
path: ex_doc
|
||||
- name: Build ex_doc
|
||||
if: ${{ inputs.build_docs }}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
|
||||
"MD001": false,
|
||||
// Header style. We use #s.
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
// Style of unordered lists..
|
||||
"MD007": {
|
||||
"indent": 2,
|
||||
"start_indented": true
|
||||
},
|
||||
// Line length. Who cares.
|
||||
"MD013": false,
|
||||
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
|
||||
// don't show output. We use those a lot, so this is fine for us.
|
||||
"MD014": false,
|
||||
// Multiple headings with the same content. That's fine.
|
||||
"MD024": false,
|
||||
// Allow empty line between block quotes. Used by contiguous admonition blocks.
|
||||
"MD028": false,
|
||||
// Allowed HTML inline elements.
|
||||
"MD033": {
|
||||
"allowed_elements": [
|
||||
"a",
|
||||
"br",
|
||||
"img",
|
||||
"noscript",
|
||||
"p",
|
||||
"script"
|
||||
]
|
||||
},
|
||||
// This warns if you have spaces in code blocks. Sometimes, that's fine.
|
||||
"MD038": false,
|
||||
// Code block style. We don't care if it's fenced or indented.
|
||||
"MD046": false
|
||||
}
|
||||
+144
-407
@@ -1,475 +1,212 @@
|
||||
# Changelog for Elixir v1.15
|
||||
# Changelog for Elixir v1.16
|
||||
|
||||
This release requires Erlang/OTP 24 and later.
|
||||
## Code snippets in diagnostics
|
||||
|
||||
Elixir v1.15 is a smaller release with focused improvements
|
||||
on compilation and boot times. This release also completes
|
||||
our integration process with Erlang/OTP logger, bringing new
|
||||
features such as log rotation and compaction out of the box.
|
||||
Elixir v1.15 introduced a new compiler diagnostic format and the ability to print multiple error diagnostics per compilation (in addition to multiple warnings).
|
||||
|
||||
You will also find additional convenience functions in `Code`,
|
||||
`Map`, `Keyword`, all Calendar modules, and others.
|
||||
With Elixir v1.16, we also include code snippets in exceptions and diagnostics raised by the compiler. For example, a syntax error now includes a pointer to where the error happened:
|
||||
|
||||
## Compile and boot-time improvements
|
||||
|
||||
The last several releases brought improvements to compilation
|
||||
time and this version is no different. In particular, Elixir
|
||||
now caches and prunes load paths before compilation, ensuring your
|
||||
project (and dependencies!) compile faster and in an environment
|
||||
closer to production.
|
||||
|
||||
In a nutshell the Erlang VM loads modules from code paths. Each
|
||||
application that ships with Erlang and Elixir plus each dependency
|
||||
become an entry in your code path. The larger the code path, the
|
||||
more work Erlang has to do in order to find a module.
|
||||
|
||||
In previous versions, Mix would only add entries to the load paths.
|
||||
Therefore, if you compiled 20 dependencies and you went to compile
|
||||
the 21st, the code path would have 21 entries (plus all Erlang and
|
||||
Elixir apps). This allowed modules from unrelated dependencies to
|
||||
be seen and made compilation slower the more dependencies you had.
|
||||
With this release, we will now prune the code paths to only the ones
|
||||
listed as dependencies, bringing the behaviour closer to `mix release`.
|
||||
|
||||
Furthermore, Erlang/OTP 26 allows us to start applications
|
||||
concurrently and cache the code path lookups, decreasing the cost of
|
||||
booting applications. The combination of Elixir v1.15 and Erlang/OTP 26
|
||||
should reduce the boot time of applications, such as when starting
|
||||
`iex -S mix` or running a single test with `mix test`, from 5% to 30%.
|
||||
|
||||
The compiler is also smarter in several ways: `@behaviour` declarations
|
||||
no longer add compile-time dependencies and aliases in patterns and
|
||||
guards add no dependency whatsoever, as no dispatching happens. Furthermore,
|
||||
Mix now tracks the digests of `@external_resource` files, reducing the
|
||||
amount of recompilation when swapping branches. Finally, dependencies
|
||||
are automatically recompiled when their compile-time configuration changes.
|
||||
|
||||
### Potential incompatibilities
|
||||
|
||||
Due to the code path pruning, if you have an application or dependency
|
||||
that does not specify its dependencies on Erlang and Elixir application,
|
||||
it may no longer compile successfully in Elixir v1.15. You can temporarily
|
||||
disable code path pruning by setting `prune_code_paths: false` in your
|
||||
`mix.exs`, although doing so may lead to runtime bugs that are only
|
||||
manifested inside a `mix release`.
|
||||
|
||||
## Compiler warnings and errors
|
||||
|
||||
The Elixir compiler can now emit many errors for a single file, making
|
||||
sure more feedback is reported to developers before compilation is aborted.
|
||||
|
||||
In Elixir v1.14, an undefined function would be reported as:
|
||||
|
||||
** (CompileError) undefined function foo/0 (there is no such import)
|
||||
my_file.exs:1
|
||||
|
||||
In Elixir v1.15, the new reports will look like:
|
||||
|
||||
error: undefined function foo/0 (there is no such import)
|
||||
my_file.exs:1
|
||||
|
||||
** (CompileError) my_file.exs: cannot compile file (errors have been logged)
|
||||
|
||||
A new function, called `Code.with_diagnostics/2`, has been added so this
|
||||
information can be leveraged by editors, allowing them to point to several
|
||||
errors at once.
|
||||
|
||||
### Potential incompatibilities
|
||||
|
||||
As part of this effort, the behaviour where undefined variables were
|
||||
transformed into nullary function calls, often leading to confusing error
|
||||
reports, has been disabled during project compilation. You can invoke
|
||||
`Code.compiler_options(on_undefined_variable: :warn)` at the top of
|
||||
your `mix.exs` to bring the old behaviour back.
|
||||
|
||||
## Integration with Erlang/OTP logger
|
||||
|
||||
This release provides additional features such as global logger
|
||||
metadata and file logging (with rotation and compaction) out-of-the-box!
|
||||
|
||||
This release also soft-deprecates Elixir's Logger Backends in
|
||||
favor of Erlang's Logger handlers. Elixir will automatically
|
||||
convert your `:console` backend configuration into the new
|
||||
configuration. Previously, you would set:
|
||||
|
||||
```elixir
|
||||
config :logger, :console,
|
||||
level: :error,
|
||||
format: "$time $message $metadata"
|
||||
```
|
||||
** (SyntaxError) invalid syntax found on lib/my_app.ex:1:17:
|
||||
error: syntax error before: '*'
|
||||
│
|
||||
1 │ [1, 2, 3, 4, 5, *]
|
||||
│ ^
|
||||
│
|
||||
└─ lib/my_app.ex:1:17
|
||||
```
|
||||
|
||||
Which is now translated to the equivalent:
|
||||
For mismatched delimiters, it now shows both delimiters:
|
||||
|
||||
```elixir
|
||||
config :logger, :default_handler,
|
||||
level: :error
|
||||
|
||||
config :logger, :default_formatter,
|
||||
format: "$time $message $metadata"
|
||||
```
|
||||
** (MismatchedDelimiterError) mismatched delimiter found on lib/my_app.ex:1:18:
|
||||
error: unexpected token: )
|
||||
│
|
||||
1 │ [1, 2, 3, 4, 5, 6)
|
||||
│ │ └ mismatched closing delimiter (expected "]")
|
||||
│ └ unclosed delimiter
|
||||
│
|
||||
└─ lib/my_app.ex:1:18
|
||||
```
|
||||
|
||||
If you use `Logger.Backends.Console` with a custom device or other
|
||||
backends, they are still fully supported and functional. If you
|
||||
implement your own backends, you want to consider migrating to
|
||||
[`:logger_backends`](https://github.com/elixir-lang/logger_backends)
|
||||
in the long term.
|
||||
For unclosed delimiters, it now shows where the unclosed delimiter starts:
|
||||
|
||||
See the new `Logger` documentation for more information on the
|
||||
new features and on compatibility.
|
||||
```
|
||||
** (TokenMissingError) token missing on lib/my_app:8:23:
|
||||
error: missing terminator: )
|
||||
│
|
||||
1 │ my_numbers = (1, 2, 3, 4, 5, 6
|
||||
│ └ unclosed delimiter
|
||||
...
|
||||
8 │ IO.inspect(my_numbers)
|
||||
│ └ missing closing delimiter (expected ")")
|
||||
│
|
||||
└─ lib/my_app:8:23
|
||||
```
|
||||
|
||||
## v1.15.8 (2024-05-21)
|
||||
Errors and warnings diagnostics also include code snippets. When possible, we will show precise spans, such as on undefined variables:
|
||||
|
||||
```
|
||||
error: undefined variable "unknown_var"
|
||||
│
|
||||
5 │ a - unknown_var
|
||||
│ ^^^^^^^^^^^
|
||||
│
|
||||
└─ lib/sample.ex:5:9: Sample.foo/1
|
||||
```
|
||||
|
||||
Otherwise the whole line is underlined:
|
||||
|
||||
```
|
||||
error: function names should start with lowercase characters or underscore, invalid name CamelCase
|
||||
│
|
||||
3 │ def CamelCase do
|
||||
│ ^^^^^^^^^^^^^^^^
|
||||
│
|
||||
└─ lib/sample.ex:3
|
||||
```
|
||||
|
||||
A huge thank you to Vinícius Müller for working on the new diagnostics.
|
||||
|
||||
## Revamped documentation
|
||||
|
||||
Elixir's Getting Started guides have been made part of the Elixir repository and incorporated into ExDoc. This was an opportunity to revisit and unify all official guides and references.
|
||||
|
||||
We have also incorporated and extended the work on [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf), by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/), into the official document in the form of anti-patterns. The anti-patterns are divided into four categories: code-related, design-related, process-related, and meta-programming. Our goal is to give all developers examples of potential anti-patterns, with context and examples on how to improve their codebases.
|
||||
|
||||
Another [ExDoc](https://github.com/elixir-lang/ex_doc) feature we have incorporated in this release is the addition of cheatsheets, starting with [a cheatsheet for the Enum module](https://hexdocs.pm/elixir/main/enum-cheat.html). If you would like to contribute future cheatsheets to Elixir itself, feel free to start a discussion with an issue.
|
||||
|
||||
Finally, we have started enriching our documentation with [Mermaid.js](https://mermaid.js.org/) diagrams. You can find examples in the [GenServer](https://hexdocs.pm/elixir/main/GenServer.html) and [Supervisor](https://hexdocs.pm/elixir/main/Supervisor.html) docs.
|
||||
|
||||
## v1.16.1 (2024-01-31)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [bin/elixir] Properly handle the `--dbg` flag in Elixir's CLI
|
||||
* [System] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows via `System.cmd/3`
|
||||
* [Port] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows
|
||||
* [URI] Ensure `:undefined` fields are properly converted to `nil` when invoking Erlang's API
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Ensure translators are persisted across logger restarts
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Ensure compile paths are accessible during compilation
|
||||
|
||||
## v1.15.7 (2023-10-14)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Elixir] Allow code evaluation across Elixir versions
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Do not emit duplicate warnings from tokenizer
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix format] Correctly match file to subdirectory in `Mix.Tasks.Format.formatter_for_file/2`
|
||||
|
||||
## v1.15.6 (2023-09-20)
|
||||
|
||||
This release also includes fixes to the Windows installer.
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Do not crash when printing tokenizer warnings
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Fix formatter for nested `*` in bitstrings
|
||||
* [Code] Improve feedback when an invalid block is given `Code.quoted_to_algebra/2`
|
||||
* [Kernel] Trace functions before they are inlined
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Ensure `:extra_applications` declare in umbrella projects are loaded
|
||||
* [mix deps.get] Do not check for invalid applications before deps.get
|
||||
* [mix deps.update] Do not check for invalid applications before deps.update
|
||||
* [mix format] Load plugins when invoking the formatter from an IDE
|
||||
|
||||
## v1.15.5 (2023-08-28)
|
||||
|
||||
### 1. Enhancements
|
||||
* [Code] Fix `Code.quoted_to_algebra/2` for operator with :do key as operand
|
||||
* [Kernel.ParallelCompiler] Do not crash parallel compiler when it receives diagnostics from additional code evaluation
|
||||
* [Kernel.ParallelCompiler] Always log errors at the end of compilation
|
||||
* [String] Fix `String.capitalize/1` with a single codepoint
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Autocomplete] Speed up loading of struct suggestions
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code.Fragment] Fix `Code.Fragment.surround_context/2` for aliases and submodules of non-aliases
|
||||
* [Kernel] Ensure stacktrace is included when necessary when rescuing multiple exceptions in the same branch
|
||||
* [Kernel] Fix index in error message for unused optional arguments
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Diff] Fix scenario where diff would not show up due to a timed-out loop
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Force group leader to run as a binary and unicode in IEx
|
||||
* [IEx] Fix autocompletion of function signatures on Erlang/OTP 26
|
||||
* [IEx] Do not assume `$HOME` is set
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Do not assume `blake` is always available
|
||||
* [mix format] Load and compile plugins if specified in subdirectories
|
||||
* [mix deps.compile] Handle compilation of rebar3 dependencies when rebar3 is on a path with spaces on Unix
|
||||
* [mix test] Properly resolve relative paths when running tests from individual files
|
||||
* [mix test] Properly resolve Windows paths when running tests from individual files
|
||||
|
||||
## v1.15.4 (2023-07-18)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix archive.build] Disable protocol consolidation when building archives on archive.install
|
||||
* [mix compile] Track removed files per local dependency (this addresses a bug where files depending on modules from path dependencies always recompiled)
|
||||
* [mix release] Do not strip relevant chunks from Erlang/OTP 26
|
||||
|
||||
## v1.15.3 (2023-07-15)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Improve stacktraces when executing unnested Elixir code in a file
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Allow to opt-out of starting apps in `Mix.install/2`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Ensure `with_diagnostics` propagate warnings from inner Erlang passes
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Fix `--remsh` on Erlang/OTP 25 and earlier
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] Ensure `__mix_recompile__?` callbacks are properly invoked
|
||||
|
||||
## v1.15.2 (2023-07-01)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Fix CLI being unable to boot on Windows
|
||||
|
||||
## v1.15.1 (2023-06-30)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
* [Code] `Code.string_to_quoted/2` honors `:static_atoms_encoder` for multi-letter sigils
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.CaptureLog] Fix race condition on concurrent `capture_log`
|
||||
* [ExUnit.CaptureLog] Respect options passed to nested `capture_log` calls
|
||||
* [ExUnit.Doctest] Properly compile doctests without results terminated by fences
|
||||
* [ExUnit.Doctest] Allow variables defined in doctests to be used in expectation
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Ensure `pry` works on Erlang/OTP 25 and earlier while IEx is booting
|
||||
* [IEx] `Code.Fragment.surround_context` considers surround context around spaces and parens
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Do not assume Logger has been loaded at compile-time
|
||||
* [Logger.Formatter] Properly handle `:function` as metadata
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Ensure the current project is available on the code path after its Elixir sources are compiled
|
||||
* [mix compile] Guarantee yecc/leex are available when emitting warnings from previous runs
|
||||
* [mix compile] Fix bug where an external resource was deleted after its
|
||||
mtime was successfully retrieved
|
||||
* [mix compile] Track removed modules and exports across local deps
|
||||
* [mix deps] Fix an issue where dependencies could not be started in an umbrella projects
|
||||
* [mix release] Properly handle optional dependencies when there is a conflict in the application start mode
|
||||
* [mix release] Remove `--werl` from release scripts on Erlang/OTP 26
|
||||
|
||||
## v1.15.0 (2023-06-19)
|
||||
## v1.16.0 (2023-12-22)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Include source code snippets in syntax errors
|
||||
* [EEx] Include relative file information in diagnostics
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Add support for epoch time (`%s`) to `Calendar.strftime/2`
|
||||
* [Code] `Code.format_string!/2` now converts `'charlists'` into `~c"charlists"` by default
|
||||
* [Code] Add `:on_undefined_variable` to the compiler options to preserve the warning behaviour which was deprecated back in Elixir v1.4
|
||||
* [Code] Add `Code.loaded?/1` and `Code.ensure_all_loaded(!)/1`
|
||||
* [Code] Add `Code.prepend_paths/1`, `Code.append_paths/1`, and `Code.delete_paths/1`
|
||||
* [Code] Add `Code.with_diagnostics/2` to return diagnostics when compiling and evaluating code
|
||||
* [Code.Fragment] Support nested expressions in `Code.Fragment.cursor_context/1`
|
||||
* [Code.Fragment] Keep operators and no paren calls in `Code.Fragment.container_cursor_to_quoted/1`
|
||||
* [Date] Add `Date.before?/2` and `Date.after?/2`
|
||||
* [DateTime] Add `DateTime.before?/2` and `DateTime.after?/2`
|
||||
* [DateTime] Support precision in `DateTime.utc_now/2`
|
||||
* [File] Support distributed `File.Stream`
|
||||
* [Inspect] `Inspect` now renders `'charlists'` as `~c"charlists"` by default
|
||||
* [Kernel] Break down `case` and `cond` inside `dbg/2`
|
||||
* [Kernel] Add `t:nonempty_binary/0` and `t:nonempty_bitstring/0`
|
||||
* [Kernel] Treat `@behaviour`s as runtime dependencies
|
||||
* [Kernel] Do not add runtime dependencies for alias references in patterns and guards
|
||||
* [Kernel] Warn for nested calls without parens inside keywords
|
||||
* [Kernel] Support for multi-letter uppercase sigils
|
||||
* [Kernel] Introduce mechanism to collect several errors in a module. Previously, as soon as there was a compilation error, compilation would fail. Now the compiler became a bit smarter and will report multiple errors whenever possible as multiple `error: ...` messages, similar to `warning: ...`
|
||||
* [Kernel] Raise instead of warning on undefined variables. Previously, an undefined variable would attempt to invoke a function of the same name, which led to confusing error messages, especially to newcomers. To enable the previous behaviour, invoke `Code.compiler_options(on_undefined_variable: :warn)` at the top of your `mix.exs`
|
||||
* [Kernel.CLI] Support `--sname undefined`/`--name undefined` so a name is automatically generated
|
||||
* [Keyword] Add `Keyword.split_with/2`
|
||||
* [Macro] Improve error message when piping into an expression ending in bracket-based access
|
||||
* [Macro.Env] Add `Macro.Env.lookup_alias_as/2`
|
||||
* [Map] Add `Map.split_with/2`
|
||||
* [Map] Add `Map.intersect/2` and `Map.intersect/3`
|
||||
* [MapSet] Add `MapSet.split_with/2`
|
||||
* [MapSet] Optimize most functions
|
||||
* [NaiveDateTime] Add `NaiveDateTime.beginning_of_day/1` and `NaiveDateTime.end_of_day/1`
|
||||
* [NaiveDateTime] Add `NaiveDateTime.before?/2` and `NaiveDateTime.after?/2`
|
||||
* [NaiveDateTime] Support precision in `NaiveDateTime.utc_now/2`
|
||||
* [Module] Mark functions as generated in "Docs" chunk
|
||||
* [Module] Add `Module.get_last_attribute/3`
|
||||
* [OptionParser] Support `:return_separator` option
|
||||
* [Process] Add `Process.alias/0,1` and `Process.unalias/1`
|
||||
* [Range] Add `Range.split/2`
|
||||
* [String] Update Unicode to version 15.0.0
|
||||
* [String] Add `:fast_ascii` mode to `String.valid?/2`
|
||||
* [Supervisor] Add support for automatic shutdown in `Supervisor`
|
||||
* [System] Support `:lines` in `System.cmd/3` to capture output line by line
|
||||
* [Task] Remove head of line blocking on `Task.yield_many/2`
|
||||
* [Task] Enable selective receive optimizations in Erlang/OTP 26+
|
||||
* [Task] Reduce tasks footprint by avoiding unecessary work during spawning
|
||||
* [Task.Supervisor] Do not copy args on temporary `Task.Supervisor.start_child/2`
|
||||
* [Time] Add `Time.before?/2` and `Time.after?/2`
|
||||
* [URI] Add `URI.append_path/2`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Add more color configuration to ExUnit CLI formatter
|
||||
* [ExUnit.Callbacks] Accept `{module, function}` tuples in ExUnit `setup` callbacks
|
||||
* [ExUnit.Case] Add `ExUnit.Case.get_last_registered_test/1`
|
||||
* [ExUnit.Doctest] Add `ExUnit.DocTest.doctest_file/2`
|
||||
* [ExUnit.Doctest] Include `doctest_data` in doctest tags
|
||||
* [ExUnit.Formatter] When comparing two anonymous functions, defined at the same place but capturing a different environment, we will now also diff the environments
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Make pry opt-in on dbg with `--dbg pry`
|
||||
* [IEX] Support `IEX_HOME`
|
||||
* [IEx.Autocomplete] Only provide aliases when autocompleting `alias`, `import`, and `require`
|
||||
* [IEx.Autocomplete] Provide field completion on map and struct updates
|
||||
* [IEx.Helpers] Add `runtime_info(:allocators)`
|
||||
* [IEx.Info] Implement protocol for `Range`, `DateTime`, and `Regex`
|
||||
* [Code] Add `:emit_warnings` for `Code.string_to_quoted/2`
|
||||
* [Code] Automatically include columns in parsing options
|
||||
* [Code] Introduce `MismatchedDelimiterError` for handling mismatched delimiter exceptions
|
||||
* [Code.Fragment] Handle anonymous calls in fragments
|
||||
* [Code.Formatter] Trim trailing whitespace on heredocs with `\r\n`
|
||||
* [File] Add `:offset` option to `File.stream!/2`
|
||||
* [Kernel] Auto infer size of matched variable in bitstrings
|
||||
* [Kernel] Preserve column information when translating typespecs
|
||||
* [Kernel] Suggest module names based on suffix and casing errors when the module does not exist in `UndefinedFunctionError`
|
||||
* [Kernel.ParallelCompiler] Introduce `Kernel.ParallelCompiler.pmap/2` to compile multiple additional entries in parallel
|
||||
* [Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias
|
||||
* [Macro] Add `Macro.compile_apply/4`
|
||||
* [Module] Add support for `@nifs` annotation from Erlang/OTP 25
|
||||
* [Module] Add support for missing `@dialyzer` configuration
|
||||
* [String] Update to Unicode 15.1.0
|
||||
* [String] Add `String.replace_invalid/2`
|
||||
* [Task] Add `:limit` option to `Task.yield_many/2`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Add `Logger.add_handlers/1` and `Logger.default_formatter/1`
|
||||
* [Logger] Introduce `default_formatter` and `default_handler` configuration for Logger which configures Erlang/OTP logger
|
||||
* [Logger] Add `:always_evaluate_messages` configuration to Logger
|
||||
* [Logger.Formatter] Implement the Erlang Logger formatter API
|
||||
* [Logger.Formatter] Add support for ports in Logger metadata
|
||||
* [Logger] Add `Logger.levels/0`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix app.start] Allow applications to be started concurrently via the `:start_concurrently` configuration
|
||||
* [mix compile] Set `--all-warnings` by default
|
||||
* [mix compile] Reduce the amount of filesystem lookups for path dependencies by storing timestamps in manifests
|
||||
* [mix compile] Track digests of `@external_resources`
|
||||
* [mix compile.app] Write `optional_applications` to `.app` file
|
||||
* [mix compile.elixir] Add `--purge-consolidation-path-if-stale` which will purge the given consolidation path if compilation is required
|
||||
* [mix deps.compile] Automatically recompile dependencies if their compile env changes
|
||||
* [mix deps.get] Automatically install Hex and Rebar on `mix deps.get`/`mix deps.update`
|
||||
* [mix deps.get] Support `--check-locked` which raises if changes to the lockfile are required
|
||||
* [mix eval] Allow passing additional arguments
|
||||
* [mix format] Support `--no-exit` option
|
||||
* [mix format] Allow multiple formatters per file extension and sigil
|
||||
* [mix format] Show diffs whenever `--check-formatted` fails
|
||||
* [mix format] Allow the formatting root to be configured
|
||||
* [mix loadpaths] Cache deps and archive loadpaths in Erlang/OTP 26
|
||||
* [mix profile.fprof] Support `--trace-to-file` to improve performance when working with large outputs
|
||||
* [mix release] Allow passing additional arguments to the `eval` command
|
||||
* [mix xref graph] Support `--output` flag
|
||||
* [Mix.Project] Support `def cli` to unify all CLI defaults in a single place
|
||||
* [Mix.Project] Add `Mix.Project.deps_tree/1`
|
||||
* [mix] Add `MIX_PROFILE` to profile a list of comma separated tasks
|
||||
* [mix archive.install] Support `--sparse` option
|
||||
* [mix compile.app] Warn if both `:applications` and `:extra_applications` are used
|
||||
* [mix compile.elixir] Pass original exception down to diagnostic `:details` when possible
|
||||
* [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed
|
||||
* [mix deps.clean] Emit a warning instead of crashing when a dependency cannot be removed
|
||||
* [mix escript.build] Escripts now strip .beam files by default, which leads to smaller escripts. However, if you are using escripts to access Elixir docs or compile Elixir code, documentation and deprecation metadata is no longer available. Set `strip_beams: false` in your escript configuration in your `mix.exs` to keep all metadata
|
||||
* [mix escript.install] Support `--sparse` option
|
||||
* [mix release] Include `include/` directory in releases
|
||||
* [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code.Formatter] Fix a scenario where a keyword followed by parenthesis could go above the maximum line length
|
||||
* [Code.Formatter] Remove unnecessary parens in nullary type funs
|
||||
* [Exception] Fix operator precedence when printing guards in `Exception.blame/3`
|
||||
* [File] Do not raise if there are file system race conditions in `File.cp/2`
|
||||
* [File] Do not raise when deleting write-only empty directories on `File.rm_rf/1`
|
||||
* [Kernel] Expand macros on the left side of -> in `try/rescue`
|
||||
* [Kernel] Raise on misplaced `...` inside typespecs
|
||||
* [Kernel] Do not import `behaviour_info` and `module_info` functions from Erlang modules
|
||||
* [Kernel] Raise when macros are given to dialyzer
|
||||
* [Kernel.ParallelCompiler] Make sure compiler doesn't crash when there are stray messages in the inbox
|
||||
* [Kernel.ParallelCompiler] Track compile and runtime warnings separately
|
||||
* [Module] Ensure that `Module.get_attribute/3` returns `nil` and not the given default value when an attribute has been explicitly set as `nil`
|
||||
* [System] Fix race condition when a script would terminate before `System.stop/1` executes
|
||||
* [Task] Do not double log Task failure reports
|
||||
* [URI] Make sure `URI.merge/2` works accordingly with relative paths
|
||||
* [Code] Keep quotes for atom keys in formatter
|
||||
* [Code.Fragment] Fix crash in `Code.Fragment.surround_context/2` when matching on `->`
|
||||
* [IO] Raise when using `IO.binwrite/2` on terminated device (mirroring `IO.write/2`)
|
||||
* [Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded)
|
||||
* [Kernel] Ensure `dbg` module is a compile-time dependency
|
||||
* [Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused
|
||||
* [Kernel] Re-enabled compiler optimizations for top level functions in scripts (disabled in v1.14.0 but shouldn't impact most programs)
|
||||
* [Kernel] Do not define an alias for nested modules starting with `Elixir.` in their definition
|
||||
* [Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks
|
||||
* [Macro] Address exception on `Macro.to_string/1` for certain ASTs
|
||||
* [Path] Lazily evaluate `File.cwd!/0` in `Path.expand/1` and `Path.absname/1`
|
||||
* [Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Fix crash when `@tag capture_log: true` was set to true and the Logger application was shut down in the middle of the test
|
||||
* [ExUnit] Do not merge context as tags inside the runner to reduce memory usage when emitting events to formatters
|
||||
* [ExUnit] Mark test cases as invalid when an exit occurs during `setup_all`
|
||||
* [ExUnit] Do not expand or collect vars from quote in ExUnit assertions
|
||||
* [ExUnit.DocTest] Ensure proper line is returned when failing to parse doctest results
|
||||
* [ExUnit.Doctest] Fix line information when a doctest with multiple assertions fails
|
||||
* [ExUnit] Raise on incorrectly dedented doctests
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Do not spawn a process to read IO. This fixes a bug where multiline paste stopped working
|
||||
whenever the input reader was killed
|
||||
* [IEx] Do not perform completion for prompts triggered during code evaluation
|
||||
* [IEx.Pry] Fix prying functions with only literals in their body
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Include `cwd` in compiler cache key
|
||||
* [mix release] Fix Windows service when invoking `erlsrv.exe` in path with spaces
|
||||
* [mix xref] Raise early if `mix xref` is used at the umbrella root
|
||||
* [mix archive.install] Restore code paths after `mix archive.install`
|
||||
* [mix compile] Ensure files with duplicate modules are recompiled whenever any of the files change
|
||||
* [mix compile] Update Mix compiler diagnostics documentation and typespecs to match the Elixir compiler behaviour where both lines and columns start from one (before it inaccurately said that columns started from zero)
|
||||
* [mix escript.install] Restore code paths after `mix escript.install`
|
||||
|
||||
### 3. Soft deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [File] `File.cp/3` and `File.cp_r/3` with a function as third argument
|
||||
is deprecated in favor of a keyword list
|
||||
* [Kernel] Require pin variable when accessing variable inside binary size in match
|
||||
* [Kernel.ParallelCompiler] Require the `:return_diagnostics` option to be
|
||||
set to true when compiling or requiring code
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] `add_backend/2`, `remove_backend/2`, and `configure_backend/2` have been deprecated
|
||||
in favor of the new `:logger_backends` dependency
|
||||
* [Logger] The `:console` configuration has been deprecated in favor of `:default_formatter`
|
||||
* [Logger] The `:backends` configuration has been deprecated in favor of `Logger.add_handlers/1`
|
||||
* [File] Deprecate `File.stream!(file, options, line_or_bytes)` in favor of keeping the options as last argument, as in `File.stream!(file, line_or_bytes, options)`
|
||||
* [Kernel.ParallelCompiler] Deprecate `Kernel.ParallelCompiler.async/1` in favor of `Kernel.ParallelCompiler.pmap/2`
|
||||
* [Path] Deprecate `Path.safe_relative_to/2` in favor of `Path.safe_relative/2`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix.Project] `:preferred_cli_env` is deprecated in favor of `:preferred_envs` in `def cli`
|
||||
* [Mix.Project] `:preferred_cli_target` is deprecated in favor of `:preferred_targets` in `def cli`
|
||||
* [mix local] The environment variable `HEX_MIRROR` is deprecated in favor of `HEX_BUILDS_URL`
|
||||
* [mix compile] Returning a four-element tuple as a position in `Mix.Task.Compiler.Diagnostic`
|
||||
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] `Calendar.ISO.day_of_week/3` is deprecated in favor of `Calendar.ISO.day_of_week/4`
|
||||
* [Exception] `Exception.exception?/1` is deprecated in favor of `Kernel.is_exception/1`
|
||||
* [Kernel] Deprecate `...` as a valid function call identifier
|
||||
* [Regex] `Regex.regex?/1` is deprecated in favor of `Kernel.is_struct/2`
|
||||
* [Date] Deprecate inferring a range with negative step, call `Date.range/3` with a negative step instead
|
||||
* [Enum] Deprecate passing a range with negative step on `Enum.slice/2`, give `first..last//1` instead
|
||||
* [Kernel] `~R/.../` is deprecated in favor of `~r/.../`. This is because `~R/.../` still allowed escape codes, which did not fit the definition of uppercase sigils
|
||||
* [String] Deprecate passing a range with negative step on `String.slice/2`, give `first..last//1` instead
|
||||
|
||||
#### Logger
|
||||
#### ExUnit
|
||||
|
||||
* [Logger] `Logger.warn/2` is deprecated in favor of `Logger.warning/2`
|
||||
* [ExUnit.Formatter] Deprecate `format_time/2`, use `format_times/1` instead
|
||||
|
||||
## v1.14
|
||||
#### Mix
|
||||
|
||||
The CHANGELOG for v1.14 releases can be found [in the v1.14 branch](https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md).
|
||||
* [mix compile.leex] Require `:leex` to be added as a compiler to run the `leex` compiler
|
||||
* [mix compile.yecc] Require `:yecc` to be added as a compiler to run the `yecc` compiler
|
||||
|
||||
## v1.15
|
||||
|
||||
The CHANGELOG for v1.15 releases can be found [in the v1.15 branch](https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md).
|
||||
|
||||
@@ -2,9 +2,7 @@ PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
CANONICAL := 1.15/
|
||||
CANONICAL ?= main/
|
||||
DOCS_FORMAT ?= html
|
||||
# CANONICAL := main/
|
||||
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
|
||||
@@ -47,7 +45,7 @@ lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1
|
||||
@ rm -rf lib/$(1)/ebin
|
||||
$(Q) cd lib/$(1) && ../../$$(ELIXIRC) "lib/**/*.ex" -o ebin
|
||||
|
||||
test_$(1): compile $(1)
|
||||
test_$(1): test_formatted $(1)
|
||||
@ echo "==> $(1) (ex_unit)"
|
||||
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
|
||||
endef
|
||||
@@ -181,7 +179,7 @@ clean_residual_files:
|
||||
|
||||
LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png")
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" --formatter "$(DOCS_FORMAT)" $(4)
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4)
|
||||
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||
|
||||
@@ -89,7 +89,7 @@ After that, clone this repository to your machine, compile and test it:
|
||||
```sh
|
||||
git clone https://github.com/elixir-lang/elixir.git
|
||||
cd elixir
|
||||
make clean test
|
||||
make
|
||||
```
|
||||
|
||||
> Note: if you are running on Windows,
|
||||
@@ -99,9 +99,7 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
|
||||
In case you want to use this Elixir version as your system version,
|
||||
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
|
||||
|
||||
If Elixir fails to build (specifically when pulling in a new version via
|
||||
`git`), be sure to remove any previous build artifacts by running
|
||||
`make clean`, then `make test`.
|
||||
Additionally, you may choose to run the test suite with `make clean test`.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -204,7 +202,7 @@ to be installed and built alongside Elixir:
|
||||
```sh
|
||||
# After cloning and compiling Elixir, in its parent directory:
|
||||
git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && ../elixir/bin/mix do deps.get + compile
|
||||
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
|
||||
```
|
||||
|
||||
Now go back to Elixir's root directory and run:
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
|
||||
### In the new branch
|
||||
|
||||
1. Set `CANONICAL=` in /Makefile
|
||||
1. Comment out `CANONICAL=` in /Makefile
|
||||
|
||||
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
|
||||
|
||||
|
||||
+5
-5
@@ -6,18 +6,18 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.15 | Bug fixes and security patches
|
||||
1.16 | Bug fixes and security patches
|
||||
1.15 | Security patches only
|
||||
1.14 | Security patches only
|
||||
1.13 | Security patches only
|
||||
1.12 | Security patches only
|
||||
1.11 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email.
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please disclose security vulnerabilities privately at elixir-security@googlegroups.com
|
||||
[Please disclose security vulnerabilities privately via GitHub](https://github.com/elixir-lang/elixir/security).
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.15.8
|
||||
ELIXIR_VERSION=1.16.1
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
@@ -112,10 +112,10 @@ while [ $I -le $LENGTH ]; do
|
||||
C=1
|
||||
MODE="iex"
|
||||
;;
|
||||
-v|--no-halt)
|
||||
-v|--no-halt|--dbg)
|
||||
C=1
|
||||
;;
|
||||
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
|
||||
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex)
|
||||
C=2
|
||||
;;
|
||||
--rpc-eval)
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||
|
||||
set ELIXIR_VERSION=1.15.8
|
||||
set ELIXIR_VERSION=1.16.1
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
@@ -140,16 +140,16 @@ if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %~1" && shift &&
|
||||
if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %~1" && shift && goto startloop)
|
||||
if ""==!par:--dbg=! (set "parsElixir=!parsElixir! --dbg %~1" && shift && goto startloop)
|
||||
rem ******* ERLANG PARAMETERS **********************
|
||||
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot %~1" && shift && goto startloop)
|
||||
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var %~1 %~2" && shift && shift && goto startloop)
|
||||
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie %~1" && shift && goto startloop)
|
||||
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop)
|
||||
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop)
|
||||
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie "%~1"" && shift && goto startloop)
|
||||
if ""==!par:--hidden=! (set "parsErlang=!parsErlang! -hidden" && goto startloop)
|
||||
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config %~1" && shift && goto startloop)
|
||||
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config "%~1"" && shift && goto startloop)
|
||||
if ""==!par:--logger-otp-reports=! (set "parsErlang=!parsErlang! -logger handle_otp_reports %1" && shift && goto startloop)
|
||||
if ""==!par:--logger-sasl-reports=! (set "parsErlang=!parsErlang! -logger handle_sasl_reports %1" && shift && goto startloop)
|
||||
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name %~1" && shift && goto startloop)
|
||||
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname %~1" && shift && goto startloop)
|
||||
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file %~1" && shift && goto startloop)
|
||||
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name "%~1"" && shift && goto startloop)
|
||||
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1"" && shift && goto startloop)
|
||||
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file "%~1"" && shift && goto startloop)
|
||||
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
|
||||
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
|
||||
set endLoop=1
|
||||
|
||||
+5
-2
@@ -1,9 +1,12 @@
|
||||
defmodule EEx.SyntaxError do
|
||||
defexception [:message, :file, :line, :column]
|
||||
defexception [:file, :line, :column, :snippet, message: "syntax error"]
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
"#{exception.file}:#{exception.line}:#{exception.column}: #{exception.message}"
|
||||
%{file: file, line: line, column: column, message: message, snippet: snippet} = exception
|
||||
|
||||
Exception.format_file_line_column(file && Path.relative_to_cwd(file), line, column, " ") <>
|
||||
message <> (snippet || "")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -489,7 +489,8 @@ defmodule EEx.Compiler do
|
||||
|
||||
defp syntax_error!(message, meta, state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: message <> code_snippet(state.source, state.indentation, meta),
|
||||
message: message,
|
||||
snippet: code_snippet(state.source, state.indentation, meta),
|
||||
file: state.file,
|
||||
line: meta.line,
|
||||
column: meta.column
|
||||
|
||||
+48
-29
@@ -273,6 +273,32 @@ defmodule EExTest do
|
||||
end
|
||||
|
||||
describe "raises syntax errors" do
|
||||
test "with relative file information" do
|
||||
message = """
|
||||
foobar.eex:1:5: expected closing '%>' for EEx expression
|
||||
|
|
||||
1 | foo <%= bar
|
||||
| ^\
|
||||
"""
|
||||
|
||||
assert_raise EEx.SyntaxError, message, fn ->
|
||||
EEx.compile_string("foo <%= bar", file: Path.join(File.cwd!(), "foobar.eex"))
|
||||
end
|
||||
end
|
||||
|
||||
test "when <%!-- is not closed" do
|
||||
message = """
|
||||
my_file.eex:1:5: expected closing '--%>' for EEx expression
|
||||
|
|
||||
1 | foo <%!-- bar
|
||||
| ^\
|
||||
"""
|
||||
|
||||
assert_raise EEx.SyntaxError, message, fn ->
|
||||
EEx.compile_string("foo <%!-- bar", file: "my_file.eex")
|
||||
end
|
||||
end
|
||||
|
||||
test "when the token is invalid" do
|
||||
message = """
|
||||
nofile:1:5: expected closing '%>' for EEx expression
|
||||
@@ -735,38 +761,31 @@ defmodule EExTest do
|
||||
end
|
||||
|
||||
test "line and column meta" do
|
||||
parser_options = Code.get_compiler_option(:parser_options)
|
||||
Code.put_compiler_option(:parser_options, columns: true)
|
||||
indentation = 12
|
||||
|
||||
try do
|
||||
indentation = 12
|
||||
ast =
|
||||
EEx.compile_string(
|
||||
"""
|
||||
<%= f() %> <% f() %>
|
||||
<%= f fn -> %>
|
||||
<%= f() %>
|
||||
<% end %>
|
||||
""",
|
||||
indentation: indentation
|
||||
)
|
||||
|
||||
ast =
|
||||
EEx.compile_string(
|
||||
"""
|
||||
<%= f() %> <% f() %>
|
||||
<%= f fn -> %>
|
||||
<%= f() %>
|
||||
<% end %>
|
||||
""",
|
||||
indentation: indentation
|
||||
)
|
||||
{_, calls} =
|
||||
Macro.prewalk(ast, [], fn
|
||||
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
|
||||
other, acc -> {other, acc}
|
||||
end)
|
||||
|
||||
{_, calls} =
|
||||
Macro.prewalk(ast, [], fn
|
||||
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
|
||||
other, acc -> {other, acc}
|
||||
end)
|
||||
|
||||
assert Enum.reverse(calls) == [
|
||||
[line: 1, column: indentation + 5],
|
||||
[line: 1, column: indentation + 15],
|
||||
[line: 2, column: indentation + 7],
|
||||
[line: 3, column: indentation + 9]
|
||||
]
|
||||
after
|
||||
Code.put_compiler_option(:parser_options, parser_options)
|
||||
end
|
||||
assert Enum.reverse(calls) == [
|
||||
[line: 1, column: indentation + 5],
|
||||
[line: 1, column: indentation + 15],
|
||||
[line: 2, column: indentation + 7],
|
||||
[line: 3, column: indentation + 9]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ defmodule Application do
|
||||
In the sections above, we have configured an application in the
|
||||
`application/0` section of the `mix.exs` file. Ultimately, Mix will use
|
||||
this configuration to create an [*application resource
|
||||
file*](https://www.erlang.org/doc/man/application.html), which is a file called
|
||||
file*](https://www.erlang.org/doc/man/app), which is a file called
|
||||
`APP_NAME.app`. For example, the application resource file of the OTP
|
||||
application `ex_unit` is called `ex_unit.app`.
|
||||
|
||||
@@ -467,6 +467,9 @@ defmodule Application do
|
||||
|
||||
* #{Enum.map_join(@application_keys, "\n * ", &"`#{inspect(&1)}`")}
|
||||
|
||||
For a description of all fields, see [Erlang's application
|
||||
specification](https://www.erlang.org/doc/man/app).
|
||||
|
||||
Note the environment is not returned as it can be accessed via
|
||||
`fetch_env/2`. Returns `nil` if the application is not loaded.
|
||||
"""
|
||||
|
||||
@@ -71,8 +71,9 @@ defmodule Date do
|
||||
A range of dates represents a discrete number of dates where
|
||||
the first and last values are dates with matching calendars.
|
||||
|
||||
Ranges of dates can be either increasing (`first <= last`) or
|
||||
decreasing (`first > last`). They are also always inclusive.
|
||||
Ranges of dates can be increasing (`first <= last`) and are
|
||||
always inclusive. For a decreasing range, use `range/3` with
|
||||
a step of -1 as first argument.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -92,8 +93,6 @@ defmodule Date do
|
||||
true
|
||||
iex> Enum.take(range, 3)
|
||||
[~D[2001-01-01], ~D[2001-01-02], ~D[2001-01-03]]
|
||||
iex> for d <- Date.range(~D[2023-03-01], ~D[2023-04-01]), Date.day_of_week(d) == 7, do: d
|
||||
[~D[2023-03-05], ~D[2023-03-12], ~D[2023-03-19], ~D[2023-03-26]]
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@@ -101,8 +100,18 @@ defmodule Date do
|
||||
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
|
||||
{first_days, _} = to_iso_days(first)
|
||||
{last_days, _} = to_iso_days(last)
|
||||
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.16
|
||||
step = if first_days <= last_days, do: 1, else: -1
|
||||
|
||||
step =
|
||||
if first_days <= last_days do
|
||||
1
|
||||
else
|
||||
IO.warn(
|
||||
"a negative range was inferred for Date.range/2, call Date.range/3 instead with -1 as third argument"
|
||||
)
|
||||
|
||||
-1
|
||||
end
|
||||
|
||||
range(first, first_days, last, last_days, calendar, step)
|
||||
end
|
||||
|
||||
@@ -569,7 +578,7 @@ defmodule Date do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the first date is strictly earlier than the second.
|
||||
Returns `true` if the first date is strictly earlier than the second.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -588,7 +597,7 @@ defmodule Date do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the first date is strictly later than the second.
|
||||
Returns `true` if the first date is strictly later than the second.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ defmodule DateTime do
|
||||
rules, ultimately affecting the result. For example, a country may
|
||||
choose to enter or abandon "Daylight Saving Time", which is a
|
||||
process where we adjust the clock one hour forward or one hour
|
||||
back once per year. Whenener the rules change, the exact instant
|
||||
back once per year. Whenever the rules change, the exact instant
|
||||
that 2:30 AM in Polish time will be in Brazil may change.
|
||||
|
||||
In other words, whenever working with future DateTimes, there is
|
||||
@@ -88,12 +88,13 @@ defmodule DateTime do
|
||||
time zone observes "Daylight Saving Time", they will move their
|
||||
clock forward once a year. When this happens, there is a whole
|
||||
hour that does not exist. Then, when they move the clock back,
|
||||
there is a certain hour that will happen twice. So if you want
|
||||
to schedule a meeting when this shift back happens, you would
|
||||
need to explicitly say which of the 2:30 AM you precisely mean.
|
||||
Applications that are date and time sensitive, need to take
|
||||
these scenarios into account and correctly communicate them to
|
||||
users.
|
||||
there is a certain hour that will happen twice. So if you want to
|
||||
schedule a meeting when this shift back happens, you would need to
|
||||
explicitly say which occurence of 2:30 AM you mean: the one in
|
||||
"Summer Time", which occurs before the shift, or the one
|
||||
in "Standard Time", which occurs after it. Applications that are
|
||||
date and time sensitive need to take these scenarios into account
|
||||
and correctly communicate them to users.
|
||||
|
||||
The good news is: Elixir contains all of the building blocks
|
||||
necessary to tackle those problems. The default timezone database
|
||||
@@ -103,6 +104,24 @@ defmodule DateTime do
|
||||
query the database and return the relevant information. For
|
||||
example, look at how `DateTime.new/4` returns different results
|
||||
based on the scenarios described in this section.
|
||||
|
||||
## Converting between timezones
|
||||
|
||||
Bearing in mind the cautions above, and assuming you've brought in a full
|
||||
timezone database, here are some examples of common shifts between time
|
||||
zones.
|
||||
|
||||
# Local time to UTC
|
||||
new_york = DateTime.from_naive!(~N[2023-06-26T09:30:00], "America/New_York")
|
||||
#=> #DateTime<2023-06-26 09:30:00-04:00 EDT America/New_York>
|
||||
|
||||
utc = DateTime.shift_zone!(new_york, "Etc/UTC")
|
||||
#=> ~U[2023-06-26 13:30:00Z]
|
||||
|
||||
# UTC to local time
|
||||
DateTime.shift_zone!(utc, "Europe/Paris")
|
||||
#=> #DateTime<2023-06-26 15:30:00+02:00 CEST Europe/Paris>
|
||||
|
||||
"""
|
||||
|
||||
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
|
||||
@@ -174,7 +193,7 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current datetime in UTC, supporting
|
||||
Returns the current datetime in UTC, supporting
|
||||
a specific calendar and precision.
|
||||
|
||||
If you want the current time in Unix seconds,
|
||||
@@ -1189,7 +1208,7 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts to ISO8601 specifying both a calendar and a mode.
|
||||
Converts from ISO8601 specifying both a calendar and a mode.
|
||||
|
||||
See `from_iso8601/2` for more information.
|
||||
|
||||
@@ -1427,7 +1446,7 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the first datetime is strictly earlier than the second.
|
||||
Returns `true` if the first datetime is strictly earlier than the second.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1446,7 +1465,7 @@ defmodule DateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the first datetime is strictly later than the second.
|
||||
Returns `true` if the first datetime is strictly later than the second.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1516,6 +1535,12 @@ defmodule DateTime do
|
||||
%{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2,
|
||||
unit
|
||||
) do
|
||||
if not is_integer(unit) and
|
||||
unit not in ~w(second millisecond microsecond nanosecond)a do
|
||||
raise ArgumentError,
|
||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||
end
|
||||
|
||||
naive_diff =
|
||||
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)) -
|
||||
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit))
|
||||
@@ -1532,10 +1557,10 @@ defmodule DateTime do
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function always consider the unit to be computed according
|
||||
This function always considers the unit to be computed according
|
||||
to the `Calendar.ISO`.
|
||||
|
||||
This function uses relies on a contiguous representation of time,
|
||||
This function relies on a contiguous representation of time,
|
||||
ignoring the wall time and timezone changes. For example, if you add
|
||||
one day when there are summer time/daylight saving time changes,
|
||||
it will also change the time forward or backward by one hour,
|
||||
|
||||
@@ -471,6 +471,12 @@ defmodule NaiveDateTime do
|
||||
unit
|
||||
)
|
||||
when is_integer(amount_to_add) do
|
||||
if not is_integer(unit) and
|
||||
unit not in ~w(second millisecond microsecond nanosecond)a do
|
||||
raise ArgumentError,
|
||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||
end
|
||||
|
||||
ppd = System.convert_time_unit(86400, :second, unit)
|
||||
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
|
||||
|
||||
@@ -554,6 +560,12 @@ defmodule NaiveDateTime do
|
||||
"and thus the result would be ambiguous"
|
||||
end
|
||||
|
||||
if not is_integer(unit) and
|
||||
unit not in ~w(second millisecond microsecond nanosecond)a do
|
||||
raise ArgumentError,
|
||||
"unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||
end
|
||||
|
||||
units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||
units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
|
||||
units1 - units2
|
||||
@@ -1073,7 +1085,7 @@ defmodule NaiveDateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the first `NaiveDateTime` is strictly earlier than the second.
|
||||
Returns `true` if the first `NaiveDateTime` is strictly earlier than the second.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1092,7 +1104,7 @@ defmodule NaiveDateTime do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the first `NaiveDateTime` is strictly later than the second.
|
||||
Returns `true` if the first `NaiveDateTime` is strictly later than the second.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1212,7 +1224,7 @@ defmodule NaiveDateTime do
|
||||
|
||||
datetime
|
||||
|> NaiveDateTime.beginning_of_day()
|
||||
|> DateTime.from_naive(datetime.timezone)
|
||||
|> DateTime.from_naive(datetime.time_zone)
|
||||
|
||||
Note that the beginning of the day may not exist or be ambiguous
|
||||
in a given timezone, so you must handle those cases accordingly.
|
||||
@@ -1239,7 +1251,7 @@ defmodule NaiveDateTime do
|
||||
|
||||
datetime
|
||||
|> NaiveDateTime.end_of_day()
|
||||
|> DateTime.from_naive(datetime.timezone)
|
||||
|> DateTime.from_naive(datetime.time_zone)
|
||||
|
||||
Note that the end of the day may not exist or be ambiguous
|
||||
in a given timezone, so you must handle those cases accordingly.
|
||||
|
||||
@@ -599,7 +599,7 @@ defmodule Time do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the first time is strictly earlier than the second.
|
||||
Returns `true` if the first time is strictly earlier than the second.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -618,7 +618,7 @@ defmodule Time do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the first time is strictly later than the second.
|
||||
Returns `true` if the first time is strictly later than the second.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
+67
-16
@@ -196,19 +196,44 @@ defmodule Code do
|
||||
|
||||
@typedoc """
|
||||
Diagnostics returned by the compiler and code evaluation.
|
||||
|
||||
The file and position relate to where the diagnostic should be shown.
|
||||
If there is a file and position, then the diagnostic is precise
|
||||
and you can use the given file and position for generating snippets,
|
||||
IDEs annotations, and so on. An optional span is available with
|
||||
the line and column the diagnostic ends.
|
||||
|
||||
Otherwise, a stacktrace may be given, which you can place your own
|
||||
heuristics to provide better reporting.
|
||||
|
||||
The source field points to the source file the compiler tracked
|
||||
the error to. For example, a file `lib/foo.ex` may embed `.eex`
|
||||
templates from `lib/foo/bar.eex`. A syntax error on the EEx template
|
||||
will point to file `lib/foo/bar.eex` but the source is `lib/foo.ex`.
|
||||
"""
|
||||
@type diagnostic(severity) :: %{
|
||||
required(:file) => Path.t(),
|
||||
required(:source) => Path.t() | nil,
|
||||
required(:file) => Path.t() | nil,
|
||||
required(:severity) => severity,
|
||||
required(:message) => String.t(),
|
||||
required(:position) => position,
|
||||
required(:position) => position(),
|
||||
required(:stacktrace) => Exception.stacktrace(),
|
||||
required(:span) => {line :: pos_integer(), column :: pos_integer()} | nil,
|
||||
optional(:details) => term(),
|
||||
optional(any()) => any()
|
||||
}
|
||||
|
||||
@typedoc "The line. 0 indicates no line."
|
||||
@type line() :: non_neg_integer()
|
||||
@type position() :: line() | {pos_integer(), column :: non_neg_integer}
|
||||
|
||||
@typedoc """
|
||||
The position of the diagnostic.
|
||||
|
||||
Can be either a line number or a `{line, column}`.
|
||||
Line and columns numbers are one-based.
|
||||
A position of `0` represents unknown.
|
||||
"""
|
||||
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
|
||||
|
||||
@boolean_compiler_options [
|
||||
:docs,
|
||||
@@ -553,14 +578,32 @@ defmodule Code do
|
||||
@doc """
|
||||
Executes the given `fun` and capture all diagnostics.
|
||||
|
||||
Diagnostics are warnings and errors emitted by the compiler
|
||||
and by functions such as `IO.warn/2`.
|
||||
Diagnostics are warnings and errors emitted during code
|
||||
evaluation or single-file compilation and by functions
|
||||
such as `IO.warn/2`.
|
||||
|
||||
If using `mix compile` or `Kernel.ParallelCompiler`,
|
||||
note they already capture and return diagnostics.
|
||||
|
||||
## Options
|
||||
|
||||
* `:log` - if the diagnostics should be logged as they happen.
|
||||
Defaults to `false`.
|
||||
|
||||
> #### Rescuing errors {: .info}
|
||||
>
|
||||
> `with_diagnostics/2` does not automatically handle exceptions.
|
||||
> You may capture them by adding a `try/1` in `fun`:
|
||||
>
|
||||
> {result, all_errors_and_warnings} =
|
||||
> Code.with_diagnostics(fn ->
|
||||
> try do
|
||||
> {:ok, Code.compile_quoted(quoted)}
|
||||
> rescue
|
||||
> err -> {:error, err}
|
||||
> end
|
||||
> end)
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
|
||||
@@ -588,11 +631,18 @@ defmodule Code do
|
||||
|
||||
A diagnostic is either returned by `Kernel.ParallelCompiler`
|
||||
or by `Code.with_diagnostics/2`.
|
||||
|
||||
## Options
|
||||
|
||||
* `:snippet` - whether to read the code snippet in the diagnostic location.
|
||||
As it may impact performance, it is not recommended to be used in runtime.
|
||||
Defaults to `true`.
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec print_diagnostic(diagnostic(:warning | :error)) :: :ok
|
||||
def print_diagnostic(diagnostic) do
|
||||
:elixir_errors.print_diagnostic(diagnostic)
|
||||
@spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
|
||||
def print_diagnostic(diagnostic, opts \\ []) do
|
||||
read_snippet? = Keyword.get(opts, :snippet, true)
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet?)
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -936,10 +986,9 @@ defmodule Code do
|
||||
to_quoted_opts =
|
||||
[
|
||||
unescape: false,
|
||||
warn_on_unnecessary_quotes: false,
|
||||
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
|
||||
token_metadata: true,
|
||||
warnings: false
|
||||
emit_warnings: false
|
||||
] ++ opts
|
||||
|
||||
{forms, comments} = string_to_quoted_with_comments!(string, to_quoted_opts)
|
||||
@@ -1044,6 +1093,8 @@ defmodule Code do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
|
||||
{term, binding, Macro.Env.t()}
|
||||
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
|
||||
when is_list(binding) do
|
||||
eval_verify(:eval_quoted, [quoted, binding, env, opts])
|
||||
@@ -1098,9 +1149,8 @@ defmodule Code do
|
||||
atoms but `:existing_atoms_only` is still used for dynamic atoms,
|
||||
such as atoms with interpolations.
|
||||
|
||||
* `:warn_on_unnecessary_quotes` - when `false`, does not warn
|
||||
when atoms, keywords or calls have unnecessary quotes on
|
||||
them. Defaults to `true`.
|
||||
* `:emit_warnings` (since v1.16.0) - when `false`, does not emit
|
||||
tokenizing/parsing related warnings. Defaults to `true`.
|
||||
|
||||
## `Macro.to_string/2`
|
||||
|
||||
@@ -1567,7 +1617,7 @@ defmodule Code do
|
||||
to the parser when compiling files. It accepts the same options as
|
||||
`string_to_quoted/2` (except by the options that change the AST itself).
|
||||
This can be used in combination with the tracer to retrieve localized
|
||||
information about events happening during compilation. Defaults to `[]`.
|
||||
information about events happening during compilation. Defaults to `[columns: true]`.
|
||||
This option only affects code compilation functions, such as `compile_string/2`
|
||||
and `compile_file/2` but not `string_to_quoted/2` and friends, as the
|
||||
latter is used for other purposes beyond compilation.
|
||||
@@ -1930,7 +1980,7 @@ defmodule Code do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the current process can await for module compilation.
|
||||
Returns `true` if the current process can await for module compilation.
|
||||
|
||||
When compiling Elixir code via `Kernel.ParallelCompiler`, which is
|
||||
used by Mix and `elixirc`, calling a module that has not yet been
|
||||
@@ -2029,7 +2079,8 @@ defmodule Code do
|
||||
|
||||
defp get_beam_and_path(module) do
|
||||
with {^module, beam, filename} <- :code.get_object_code(module),
|
||||
{:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do
|
||||
info_pairs when is_list(info_pairs) <- :beam_lib.info(beam),
|
||||
{:ok, ^module} <- Keyword.fetch(info_pairs, :module) do
|
||||
{beam, filename}
|
||||
else
|
||||
_ -> :error
|
||||
|
||||
@@ -515,19 +515,20 @@ defmodule Code.Formatter do
|
||||
if keyword_key?(left_arg) do
|
||||
{left, state} =
|
||||
case left_arg do
|
||||
# TODO: Remove this clause in v1.16 when we no longer quote operator :..//
|
||||
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
|
||||
{:__block__, _, [:"..//"]} ->
|
||||
{string(~S{"..//":}), state}
|
||||
|
||||
{:__block__, _, [atom]} when is_atom(atom) ->
|
||||
key =
|
||||
iodata =
|
||||
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
|
||||
IO.iodata_to_binary([Atom.to_string(atom), ?:])
|
||||
[Atom.to_string(atom), ?:]
|
||||
else
|
||||
IO.iodata_to_binary([?", Atom.to_string(atom), ?", ?:])
|
||||
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
|
||||
end
|
||||
|
||||
{string(key) |> color(:atom, state.inspect_opts), state}
|
||||
{iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts),
|
||||
state}
|
||||
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
|
||||
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
|
||||
@@ -1561,7 +1562,7 @@ defmodule Code.Formatter do
|
||||
Atom.to_string(nil) |> color(nil, inspect_opts)
|
||||
end
|
||||
|
||||
# TODO: Remove this clause in v1.16 when we no longer quote operator :..//
|
||||
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
|
||||
defp atom_to_algebra(:"..//", _, inspect_opts) do
|
||||
string(":\"..//\"") |> color(:atom, inspect_opts)
|
||||
end
|
||||
@@ -1622,25 +1623,30 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp insert_underscores(digits) do
|
||||
byte_size = byte_size(digits)
|
||||
|
||||
cond do
|
||||
digits =~ "_" ->
|
||||
digits
|
||||
|
||||
byte_size(digits) >= 6 ->
|
||||
digits
|
||||
|> String.to_charlist()
|
||||
|> Enum.reverse()
|
||||
|> Enum.chunk_every(3)
|
||||
|> Enum.intersperse(~c"_")
|
||||
|> List.flatten()
|
||||
|> Enum.reverse()
|
||||
|> List.to_string()
|
||||
byte_size >= 6 ->
|
||||
offset = rem(byte_size, 3)
|
||||
{prefix, rest} = String.split_at(digits, offset)
|
||||
do_insert_underscores(prefix, rest)
|
||||
|
||||
true ->
|
||||
digits
|
||||
end
|
||||
end
|
||||
|
||||
defp do_insert_underscores(acc, ""), do: acc
|
||||
|
||||
defp do_insert_underscores("", <<next::binary-3, rest::binary>>),
|
||||
do: do_insert_underscores(next, rest)
|
||||
|
||||
defp do_insert_underscores(acc, <<next::binary-3, rest::binary>>),
|
||||
do: do_insert_underscores(<<acc::binary, "_", next::binary>>, rest)
|
||||
|
||||
defp escape_heredoc(string, escape) do
|
||||
string = String.replace(string, escape, "\\" <> escape)
|
||||
heredoc_to_algebra(["" | String.split(string, "\n")])
|
||||
@@ -1678,6 +1684,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp heredoc_line(["", _ | _]), do: nest(line(), :reset)
|
||||
defp heredoc_line(["\r", _ | _]), do: nest(line(), :reset)
|
||||
defp heredoc_line(_), do: line()
|
||||
|
||||
defp args_to_algebra_with_comments(args, meta, skip_parens?, last_arg_mode, join, state, fun) do
|
||||
|
||||
@@ -78,6 +78,9 @@ defmodule Code.Fragment do
|
||||
* `{:local_call, charlist}` - the context is a local (import or local)
|
||||
call, such as `hello_world(` and `hello_world `
|
||||
|
||||
* `{:anonymous_call, inside_caller}` - the context is an anonymous
|
||||
call, such as `fun.(` and `@fun.(`.
|
||||
|
||||
* `{:module_attribute, charlist}` - the context is a module attribute,
|
||||
such as `@hello_wor`
|
||||
|
||||
@@ -140,6 +143,7 @@ defmodule Code.Fragment do
|
||||
| {:local_or_var, charlist}
|
||||
| {:local_arity, charlist}
|
||||
| {:local_call, charlist}
|
||||
| {:anonymous_call, inside_caller}
|
||||
| {:module_attribute, charlist}
|
||||
| {:operator, charlist}
|
||||
| {:operator_arity, charlist}
|
||||
@@ -164,7 +168,8 @@ defmodule Code.Fragment do
|
||||
| {:alias, inside_alias, charlist}
|
||||
| {:local_or_var, charlist}
|
||||
| {:module_attribute, charlist}
|
||||
| {:dot, inside_dot, charlist}
|
||||
| {:dot, inside_dot, charlist},
|
||||
inside_caller: {:var, charlist} | {:module_attribute, charlist}
|
||||
def cursor_context(fragment, opts \\ [])
|
||||
|
||||
def cursor_context(fragment, opts)
|
||||
@@ -242,11 +247,22 @@ defmodule Code.Fragment do
|
||||
end
|
||||
|
||||
defp call_to_cursor_context({reverse, spaces}) do
|
||||
case identifier_to_cursor_context(reverse, spaces, true) do
|
||||
{{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
|
||||
{{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
|
||||
{{:operator, acc}, count} -> {{:operator_call, acc}, count}
|
||||
{_, _} -> {:none, 0}
|
||||
with [?. | rest] <- reverse,
|
||||
{rest, spaces} = strip_spaces(rest, spaces),
|
||||
[h | _] when h not in @non_identifier <- rest do
|
||||
case identifier_to_cursor_context(rest, spaces, true) do
|
||||
{{:local_or_var, acc}, count} -> {{:anonymous_call, {:var, acc}}, count + 1}
|
||||
{{:module_attribute, _} = attr, count} -> {{:anonymous_call, attr}, count + 1}
|
||||
{_, _} -> {:none, 0}
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
case identifier_to_cursor_context(reverse, spaces, true) do
|
||||
{{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
|
||||
{{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
|
||||
{{:operator, acc}, count} -> {{:operator_call, acc}, count}
|
||||
{_, _} -> {:none, 0}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -473,7 +489,7 @@ defmodule Code.Fragment do
|
||||
|
||||
cond do
|
||||
Code.Identifier.unary_op(op) == :error and Code.Identifier.binary_op(op) == :error ->
|
||||
:none
|
||||
{:none, 0}
|
||||
|
||||
match?([?. | rest] when rest == [] or hd(rest) != ?., rest) ->
|
||||
dot(tl(rest), dot_count + 1, acc)
|
||||
@@ -581,7 +597,8 @@ defmodule Code.Fragment do
|
||||
| {:dot, inside_dot, charlist}
|
||||
| {:module_attribute, charlist}
|
||||
| {:unquoted_atom, charlist}
|
||||
| {:var, charlist},
|
||||
| {:var, charlist}
|
||||
| :expr,
|
||||
inside_alias:
|
||||
{:local_or_var, charlist}
|
||||
| {:module_attribute, charlist},
|
||||
@@ -619,7 +636,7 @@ defmodule Code.Fragment do
|
||||
{reversed_pre, post} = adjust_position(reversed_pre, post)
|
||||
|
||||
case take_identifier(post, []) do
|
||||
:none ->
|
||||
{_, [], _} ->
|
||||
maybe_operator(reversed_pre, post, line, opts)
|
||||
|
||||
{:identifier, reversed_post, rest} ->
|
||||
@@ -627,7 +644,7 @@ defmodule Code.Fragment do
|
||||
reversed = reversed_post ++ reversed_pre
|
||||
|
||||
case codepoint_cursor_context(reversed, opts) do
|
||||
{{:struct, acc}, offset} when acc != [] ->
|
||||
{{:struct, acc}, offset} ->
|
||||
build_surround({:struct, acc}, reversed, line, offset)
|
||||
|
||||
{{:alias, acc}, offset} ->
|
||||
@@ -732,27 +749,11 @@ defmodule Code.Fragment do
|
||||
do: take_identifier(t, [h | acc])
|
||||
|
||||
defp take_identifier(rest, acc) do
|
||||
{stripped, _} = strip_spaces(rest, 0)
|
||||
|
||||
with [?. | t] <- stripped,
|
||||
with {[?. | t], _} <- strip_spaces(rest, 0),
|
||||
{[h | _], _} when h in ?A..?Z <- strip_spaces(t, 0) do
|
||||
take_alias(rest, acc)
|
||||
else
|
||||
# Consider it an identifier if we are at the end of line
|
||||
# or if we have spaces not followed by . (call) or / (arity)
|
||||
_ when acc == [] and (rest == [] or (hd(rest) in @space and hd(stripped) not in ~c"/.")) ->
|
||||
{:identifier, acc, rest}
|
||||
|
||||
# If we are immediately followed by a container, we are still part of the identifier.
|
||||
# We don't consider << as it _may_ be an operator.
|
||||
_ when acc == [] and hd(stripped) in ~c"({[" ->
|
||||
{:identifier, acc, rest}
|
||||
|
||||
_ when acc == [] ->
|
||||
:none
|
||||
|
||||
_ ->
|
||||
{:identifier, acc, rest}
|
||||
_ -> {:identifier, acc, rest}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1090,6 +1091,6 @@ defmodule Code.Fragment do
|
||||
opts =
|
||||
Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder])
|
||||
|
||||
Code.string_to_quoted(fragment, [cursor_completion: true, warnings: false] ++ opts)
|
||||
Code.string_to_quoted(fragment, [cursor_completion: true, emit_warnings: false] ++ opts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -349,19 +349,25 @@ defmodule Code.Normalizer do
|
||||
meta
|
||||
end
|
||||
|
||||
last = List.last(args)
|
||||
|
||||
cond do
|
||||
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], List.last(args)) ->
|
||||
not allow_keyword?(form, arity) ->
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
|
||||
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) ->
|
||||
# def foo do :ok end
|
||||
# def foo, do: :ok
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
match?([{:do, _} | _], List.last(args)) ->
|
||||
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
|
||||
# Non normalized kw blocks
|
||||
line = state.parent_meta[:line]
|
||||
meta = meta ++ [do: [line: line], end: [line: line]]
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
allow_keyword?(form, arity) ->
|
||||
true ->
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{last_arg, leading_args} = List.pop_at(args, -1, [])
|
||||
|
||||
@@ -382,10 +388,6 @@ defmodule Code.Normalizer do
|
||||
end
|
||||
|
||||
{form, meta, leading_args ++ last_args}
|
||||
|
||||
true ->
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ defmodule Code.Typespec do
|
||||
located by the runtime system. The types will be in the Erlang
|
||||
Abstract Format.
|
||||
"""
|
||||
@spec fetch_specs(module) :: {:ok, [tuple]} | :error
|
||||
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
|
||||
def fetch_specs(module) when is_atom(module) or is_binary(module) do
|
||||
case typespecs_abstract_code(module) do
|
||||
{:ok, abstract_code} ->
|
||||
@@ -142,7 +142,7 @@ defmodule Code.Typespec do
|
||||
which can be located by the runtime system. The types will be
|
||||
in the Erlang Abstract Format.
|
||||
"""
|
||||
@spec fetch_callbacks(module) :: {:ok, [tuple]} | :error
|
||||
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
|
||||
def fetch_callbacks(module) when is_atom(module) or is_binary(module) do
|
||||
case typespecs_abstract_code(module) do
|
||||
{:ok, abstract_code} ->
|
||||
@@ -175,7 +175,8 @@ defmodule Code.Typespec do
|
||||
|
||||
defp get_module_and_beam(module) when is_atom(module) do
|
||||
with {^module, beam, _filename} <- :code.get_object_code(module),
|
||||
{:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do
|
||||
info_pairs when is_list(info_pairs) <- :beam_lib.info(beam),
|
||||
{:ok, ^module} <- Keyword.fetch(info_pairs, :module) do
|
||||
{module, beam}
|
||||
else
|
||||
_ -> :error
|
||||
@@ -419,5 +420,13 @@ defmodule Code.Typespec do
|
||||
:error
|
||||
end
|
||||
|
||||
defp meta(anno), do: [line: :erl_anno.line(anno)]
|
||||
defp meta(anno) do
|
||||
case :erl_anno.location(anno) do
|
||||
{line, column} ->
|
||||
[line: line, column: column]
|
||||
|
||||
line when is_integer(line) ->
|
||||
[line: line]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,9 +46,9 @@ defmodule Config do
|
||||
## Migrating from `use Mix.Config`
|
||||
|
||||
The `Config` module in Elixir was introduced in v1.9 as a replacement to
|
||||
`Mix.Config`, which was specific to Mix and has been deprecated.
|
||||
`use Mix.Config`, which was specific to Mix and has been deprecated.
|
||||
|
||||
You can leverage `Config` instead of `Mix.Config` in three steps. The first
|
||||
You can leverage `Config` instead of `use Mix.Config` in three steps. The first
|
||||
step is to replace `use Mix.Config` at the top of your config files by
|
||||
`import Config`.
|
||||
|
||||
@@ -86,7 +86,7 @@ defmodule Config do
|
||||
the `mix.exs` file and inside custom Mix tasks, which always within the
|
||||
`Mix.Tasks` namespace.
|
||||
|
||||
## config/runtime.exs
|
||||
## `config/runtime.exs`
|
||||
|
||||
For runtime configuration, you can use the `config/runtime.exs` file.
|
||||
It is executed right before applications start in both Mix and releases
|
||||
|
||||
+38
-26
@@ -256,10 +256,13 @@ defmodule Enum do
|
||||
iex> Enum.map(map, fn {k, v} -> {k, v * 2} end)
|
||||
[{"a", 2}, {"b", 4}]
|
||||
|
||||
However, many other enumerables exist in the language, such as `MapSet`s
|
||||
Many other enumerables exist in the language, such as `MapSet`s
|
||||
and the data type returned by `File.stream!/3` which allows a file to be
|
||||
traversed as if it was an enumerable.
|
||||
|
||||
For a general overview of all functions in the `Enum` module, see
|
||||
[the `Enum` cheatsheet](enum-cheat.cheatmd).
|
||||
|
||||
The functions in this module work in linear time. This means that, the
|
||||
time it takes to perform an operation grows at the same rate as the length
|
||||
of the enumerable. This is expected on operations such as `Enum.map/2`.
|
||||
@@ -812,11 +815,11 @@ defmodule Enum do
|
||||
|
||||
@doc """
|
||||
Enumerates the `enumerable`, returning a list where all consecutive
|
||||
duplicated elements are collapsed to a single element.
|
||||
duplicate elements are collapsed to a single element.
|
||||
|
||||
Elements are compared using `===/2`.
|
||||
|
||||
If you want to remove all duplicated elements, regardless of order,
|
||||
If you want to remove all duplicate elements, regardless of order,
|
||||
see `uniq/1`.
|
||||
|
||||
## Examples
|
||||
@@ -845,7 +848,7 @@ defmodule Enum do
|
||||
|
||||
@doc """
|
||||
Enumerates the `enumerable`, returning a list where all consecutive
|
||||
duplicated elements are collapsed to a single element.
|
||||
duplicate elements are collapsed to a single element.
|
||||
|
||||
The function `fun` maps every element to a term which is used to
|
||||
determine if two elements are duplicates.
|
||||
@@ -1216,7 +1219,8 @@ defmodule Enum do
|
||||
"no bools!"
|
||||
|
||||
"""
|
||||
@spec find_value(t, any, (element -> any)) :: any | nil
|
||||
@spec find_value(t, default, (element -> found_value)) :: found_value | default
|
||||
when found_value: term
|
||||
def find_value(enumerable, default \\ nil, fun)
|
||||
|
||||
def find_value(enumerable, default, fun) when is_list(enumerable) do
|
||||
@@ -1489,6 +1493,9 @@ defmodule Enum do
|
||||
iex> Enum.into([a: 1, a: 2], %{})
|
||||
%{a: 2}
|
||||
|
||||
iex> Enum.into([a: 2], %{a: 1, b: 3})
|
||||
%{a: 2, b: 3}
|
||||
|
||||
"""
|
||||
@spec into(Enumerable.t(), Collectable.t()) :: Collectable.t()
|
||||
def into(enumerable, collectable)
|
||||
@@ -2386,7 +2393,14 @@ defmodule Enum do
|
||||
def random(enumerable) when is_list(enumerable) do
|
||||
case length(enumerable) do
|
||||
0 -> raise Enum.EmptyError
|
||||
length -> enumerable |> drop_list(random_integer(0, length - 1)) |> hd()
|
||||
length -> enumerable |> drop_list(random_count(length)) |> hd()
|
||||
end
|
||||
end
|
||||
|
||||
def random(first.._//step = range) do
|
||||
case Range.size(range) do
|
||||
0 -> raise Enum.EmptyError
|
||||
size -> first + random_count(size) * step
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2397,14 +2411,14 @@ defmodule Enum do
|
||||
[]
|
||||
|
||||
{:ok, count, fun} when is_function(fun, 1) ->
|
||||
slice_list(fun.(enumerable), random_integer(0, count - 1), 1, 1)
|
||||
slice_list(fun.(enumerable), random_count(count), 1, 1)
|
||||
|
||||
# TODO: Deprecate me in Elixir v1.18.
|
||||
{:ok, count, fun} when is_function(fun, 2) ->
|
||||
fun.(random_integer(0, count - 1), 1)
|
||||
fun.(random_count(count), 1)
|
||||
|
||||
{:ok, count, fun} when is_function(fun, 3) ->
|
||||
fun.(random_integer(0, count - 1), 1, 1)
|
||||
fun.(random_count(count), 1, 1)
|
||||
|
||||
{:error, _} ->
|
||||
take_random(enumerable, 1)
|
||||
@@ -2416,6 +2430,10 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
defp random_count(count) do
|
||||
:rand.uniform(count) - 1
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invokes `fun` for each element in the `enumerable` with the
|
||||
accumulator.
|
||||
@@ -2960,20 +2978,23 @@ defmodule Enum do
|
||||
[]
|
||||
|
||||
# first is greater than last
|
||||
iex> Enum.slice([1, 2, 3, 4, 5], 6..5)
|
||||
iex> Enum.slice([1, 2, 3, 4, 5], 6..5//1)
|
||||
[]
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec slice(t, Range.t()) :: list
|
||||
def slice(enumerable, first..last//step = index_range) do
|
||||
# TODO: Deprecate negative steps on Elixir v1.16
|
||||
# TODO: Support negative steps as a reverse on Elixir v2.0.
|
||||
cond do
|
||||
step > 0 ->
|
||||
slice_range(enumerable, first, last, step)
|
||||
|
||||
step == -1 and first > last ->
|
||||
IO.warn(
|
||||
"negative steps are not supported in Enum.slice/2, pass #{first}..#{last}//1 instead"
|
||||
)
|
||||
|
||||
slice_range(enumerable, first, last, 1)
|
||||
|
||||
true ->
|
||||
@@ -3602,7 +3623,7 @@ defmodule Enum do
|
||||
sample = Tuple.duplicate(nil, count)
|
||||
|
||||
reducer = fn elem, {idx, sample} ->
|
||||
jdx = random_integer(0, idx)
|
||||
jdx = random_index(idx)
|
||||
|
||||
cond do
|
||||
idx < count ->
|
||||
@@ -3623,7 +3644,7 @@ defmodule Enum do
|
||||
|
||||
def take_random(enumerable, count) when is_integer(count) and count >= 0 do
|
||||
reducer = fn elem, {idx, sample} ->
|
||||
jdx = random_integer(0, idx)
|
||||
jdx = random_index(idx)
|
||||
|
||||
cond do
|
||||
idx < count ->
|
||||
@@ -3659,6 +3680,9 @@ defmodule Enum do
|
||||
|
||||
defp take_random_list_one([], current, _), do: [current]
|
||||
|
||||
defp random_index(0), do: 0
|
||||
defp random_index(idx), do: :rand.uniform(idx + 1) - 1
|
||||
|
||||
@doc """
|
||||
Takes the elements from the beginning of the `enumerable` while `fun` returns
|
||||
a truthy value.
|
||||
@@ -3704,7 +3728,7 @@ defmodule Enum do
|
||||
def to_list(enumerable), do: reverse(enumerable) |> :lists.reverse()
|
||||
|
||||
@doc """
|
||||
Enumerates the `enumerable`, removing all duplicated elements.
|
||||
Enumerates the `enumerable`, removing all duplicate elements.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -4142,18 +4166,6 @@ defmodule Enum do
|
||||
end)
|
||||
end
|
||||
|
||||
defp random_integer(limit, limit) when is_integer(limit) do
|
||||
limit
|
||||
end
|
||||
|
||||
defp random_integer(lower_limit, upper_limit) when upper_limit < lower_limit do
|
||||
random_integer(upper_limit, lower_limit)
|
||||
end
|
||||
|
||||
defp random_integer(lower_limit, upper_limit) do
|
||||
lower_limit + :rand.uniform(upper_limit - lower_limit + 1) - 1
|
||||
end
|
||||
|
||||
## Implementations
|
||||
|
||||
## all?/1
|
||||
|
||||
+728
-33
File diff suppressed because it is too large
Load Diff
+61
-8
@@ -74,6 +74,29 @@ defmodule File do
|
||||
|
||||
Check `:file.open/2` for more information about such options and
|
||||
other performance considerations.
|
||||
|
||||
## Seeking within a file
|
||||
|
||||
You may also use any of the functions from the [`:file`](`:file`)
|
||||
module to interact with files returned by Elixir. For example,
|
||||
to read from a specific position in a file, use `:file.pread/3`:
|
||||
|
||||
File.write!("example.txt", "Eats, Shoots & Leaves")
|
||||
file = File.open!("example.txt")
|
||||
:file.pread(file, 15, 6)
|
||||
#=> {:ok, "Leaves"}
|
||||
|
||||
Alternatively, if you need to keep track of the current position,
|
||||
use `:file.position/2` and `:file.read/2`:
|
||||
|
||||
:file.position(file, 6)
|
||||
#=> {:ok, 6}
|
||||
:file.read(file, 6)
|
||||
#=> {:ok, "Shoots"}
|
||||
:file.position(file, {:cur, -12})
|
||||
#=> {:ok, 0}
|
||||
:file.read(file, 4)
|
||||
#=> {:ok, "Eats"}
|
||||
"""
|
||||
|
||||
@type posix :: :file.posix()
|
||||
@@ -110,6 +133,7 @@ defmodule File do
|
||||
|
||||
@type stream_mode ::
|
||||
encoding_mode()
|
||||
| read_offset_mode()
|
||||
| :append
|
||||
| :compressed
|
||||
| :delayed_write
|
||||
@@ -117,6 +141,8 @@ defmodule File do
|
||||
| {:read_ahead, pos_integer | false}
|
||||
| {:delayed_write, non_neg_integer, non_neg_integer}
|
||||
|
||||
@type read_offset_mode :: {:read_offset, non_neg_integer()}
|
||||
|
||||
@type erlang_time ::
|
||||
{{year :: non_neg_integer(), month :: 1..12, day :: 1..31},
|
||||
{hour :: 0..23, minute :: 0..59, second :: 0..59}}
|
||||
@@ -1630,6 +1656,11 @@ defmodule File do
|
||||
@doc """
|
||||
Returns the list of files in the given directory.
|
||||
|
||||
Hidden files are not ignored and the results are *not* sorted.
|
||||
|
||||
Since directories are considered files by the file system,
|
||||
they are also included in the returned value.
|
||||
|
||||
Returns `{:ok, files}` in case of success,
|
||||
`{:error, reason}` otherwise.
|
||||
"""
|
||||
@@ -1671,6 +1702,18 @@ defmodule File do
|
||||
:file.close(io_device)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shortcut for `File.stream!/3`.
|
||||
"""
|
||||
@spec stream!(Path.t(), :line | pos_integer | [stream_mode]) :: File.Stream.t()
|
||||
def stream!(path, line_or_bytes_modes \\ [])
|
||||
|
||||
def stream!(path, modes) when is_list(modes),
|
||||
do: stream!(path, :line, modes)
|
||||
|
||||
def stream!(path, line_or_bytes) when is_integer(line_or_bytes) or line_or_bytes == :line,
|
||||
do: stream!(path, line_or_bytes, [])
|
||||
|
||||
@doc ~S"""
|
||||
Returns a `File.Stream` for the given `path` with the given `modes`.
|
||||
|
||||
@@ -1708,27 +1751,37 @@ defmodule File do
|
||||
One may also consider passing the `:delayed_write` option if the stream
|
||||
is meant to be written to under a tight loop.
|
||||
|
||||
## Byte order marks
|
||||
## Byte order marks and read offset
|
||||
|
||||
If you pass `:trim_bom` in the modes parameter, the stream will
|
||||
trim UTF-8, UTF-16 and UTF-32 byte order marks when reading from file.
|
||||
|
||||
Note that this function does not try to discover the file encoding
|
||||
based on BOM.
|
||||
based on BOM. From Elixir v1.16.0, you may also pass a `:read_offset`
|
||||
that is skipped whenever enumerating the stream (if both `:read_offset`
|
||||
and `:trim_bom` are given, the offset is skipped after the BOM).
|
||||
|
||||
## Examples
|
||||
|
||||
# Read a utf8 text file which may include BOM
|
||||
File.stream!("./test/test.txt", encoding: :utf8, trim_bom: true)
|
||||
|
||||
# Read in 2048 byte chunks rather than lines
|
||||
File.stream!("./test/test.data", [], 2048)
|
||||
#=> %File.Stream{line_or_bytes: 2048, modes: [:raw, :read_ahead, :binary],
|
||||
#=> path: "./test/test.data", raw: true}
|
||||
File.stream!("./test/test.data", 2048)
|
||||
|
||||
See `Stream.run/1` for an example of streaming into a file.
|
||||
"""
|
||||
@spec stream!(Path.t(), [stream_mode], :line | pos_integer) :: File.Stream.t()
|
||||
def stream!(path, modes \\ [], line_or_bytes \\ :line) do
|
||||
@spec stream!(Path.t(), :line | pos_integer, [stream_mode]) :: File.Stream.t()
|
||||
def stream!(path, line_or_bytes, modes)
|
||||
|
||||
def stream!(path, modes, line_or_bytes) when is_list(modes) do
|
||||
# TODO: Deprecate this on Elixir v1.20
|
||||
stream!(path, line_or_bytes, modes)
|
||||
end
|
||||
|
||||
def stream!(path, line_or_bytes, modes) do
|
||||
modes = normalize_modes(modes, true)
|
||||
File.Stream.__build__(IO.chardata_to_string(path), modes, line_or_bytes)
|
||||
File.Stream.__build__(IO.chardata_to_string(path), line_or_bytes, modes)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -17,7 +17,13 @@ defmodule File.Stream do
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
@doc false
|
||||
def __build__(path, modes, line_or_bytes) do
|
||||
def __build__(path, line_or_bytes, modes) do
|
||||
with {:read_offset, offset} <- :lists.keyfind(:read_offset, 1, modes),
|
||||
false <- is_integer(offset) and offset >= 0 do
|
||||
raise ArgumentError,
|
||||
"expected :read_offset to be a non-negative integer, got: #{inspect(offset)}"
|
||||
end
|
||||
|
||||
raw = :lists.keyfind(:encoding, 1, modes) == false
|
||||
|
||||
modes =
|
||||
@@ -88,7 +94,7 @@ defmodule File.Stream do
|
||||
start_fun = fn ->
|
||||
case File.Stream.__open__(stream, read_modes(modes)) do
|
||||
{:ok, device} ->
|
||||
if :trim_bom in modes, do: trim_bom(device, raw) |> elem(0), else: device
|
||||
skip_bom_and_offset(device, raw, modes)
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: stream.path
|
||||
@@ -104,9 +110,14 @@ defmodule File.Stream do
|
||||
Stream.resource(start_fun, next_fun, &:file.close/1).(acc, fun)
|
||||
end
|
||||
|
||||
def count(%{modes: modes, line_or_bytes: :line, path: path} = stream) do
|
||||
def count(%{modes: modes, line_or_bytes: :line, path: path, raw: raw} = stream) do
|
||||
pattern = :binary.compile_pattern("\n")
|
||||
counter = &count_lines(&1, path, pattern, read_function(stream), 0)
|
||||
|
||||
counter = fn device ->
|
||||
device = skip_bom_and_offset(device, raw, modes)
|
||||
count_lines(device, path, pattern, read_function(stream), 0)
|
||||
end
|
||||
|
||||
{:ok, open!(stream, modes, counter)}
|
||||
end
|
||||
|
||||
@@ -116,8 +127,11 @@ defmodule File.Stream do
|
||||
{:error, __MODULE__}
|
||||
|
||||
{:ok, %{size: size}} ->
|
||||
bom_offset = count_raw_bom(stream, modes)
|
||||
offset = get_read_offset(modes)
|
||||
size = max(size - bom_offset - offset, 0)
|
||||
remainder = if rem(size, bytes) == 0, do: 0, else: 1
|
||||
{:ok, div(size, bytes) + remainder - count_raw_bom(stream, modes)}
|
||||
{:ok, div(size, bytes) + remainder}
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "stream", path: path
|
||||
@@ -158,6 +172,23 @@ defmodule File.Stream do
|
||||
end
|
||||
end
|
||||
|
||||
defp skip_bom_and_offset(device, raw, modes) do
|
||||
device =
|
||||
if :trim_bom in modes do
|
||||
device |> trim_bom(raw) |> elem(0)
|
||||
else
|
||||
device
|
||||
end
|
||||
|
||||
offset = get_read_offset(modes)
|
||||
|
||||
if offset > 0 do
|
||||
{:ok, _} = :file.position(device, {:cur, offset})
|
||||
end
|
||||
|
||||
device
|
||||
end
|
||||
|
||||
defp trim_bom(device, true) do
|
||||
bom_length = device |> IO.binread(4) |> bom_length()
|
||||
{:ok, new_pos} = :file.position(device, bom_length)
|
||||
@@ -183,6 +214,13 @@ defmodule File.Stream do
|
||||
defp bom_length(<<254, 255, 0, 0, _rest::binary>>), do: 4
|
||||
defp bom_length(_binary), do: 0
|
||||
|
||||
def get_read_offset(modes) do
|
||||
case :lists.keyfind(:read_offset, 1, modes) do
|
||||
{:read_offset, offset} -> offset
|
||||
false -> 0
|
||||
end
|
||||
end
|
||||
|
||||
defp read_modes(modes) do
|
||||
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
|
||||
end
|
||||
|
||||
@@ -61,6 +61,7 @@ defmodule Float do
|
||||
1.7976931348623157e308
|
||||
|
||||
"""
|
||||
@spec max_finite() :: float
|
||||
def max_finite, do: @max_finite
|
||||
|
||||
@doc """
|
||||
@@ -72,6 +73,7 @@ defmodule Float do
|
||||
-1.7976931348623157e308
|
||||
|
||||
"""
|
||||
@spec min_finite() :: float
|
||||
def min_finite, do: @min_finite
|
||||
|
||||
@doc """
|
||||
@@ -196,7 +198,7 @@ defmodule Float do
|
||||
defp add_dot(acc, false), do: acc <> ".0"
|
||||
|
||||
@doc """
|
||||
Rounds a float to the largest number less than or equal to `num`.
|
||||
Rounds a float to the largest float less than or equal to `number`.
|
||||
|
||||
`floor/2` also accepts a precision to round a floating-point value down
|
||||
to an arbitrary number of fractional digits (between 0 and 15).
|
||||
@@ -244,7 +246,7 @@ defmodule Float do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Rounds a float to the smallest integer greater than or equal to `num`.
|
||||
Rounds a float to the smallest float greater than or equal to `number`.
|
||||
|
||||
`ceil/2` also accepts a precision to round a floating-point value down
|
||||
to an arbitrary number of fractional digits (between 0 and 15).
|
||||
@@ -350,7 +352,7 @@ defmodule Float do
|
||||
raise ArgumentError, invalid_precision_message(precision)
|
||||
end
|
||||
|
||||
defp round(0.0 = num, _precision, _rounding), do: num
|
||||
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
|
||||
|
||||
defp round(float, precision, rounding) do
|
||||
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
|
||||
@@ -498,7 +500,7 @@ defmodule Float do
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec ratio(float) :: {integer, pos_integer}
|
||||
def ratio(0.0), do: {0, 1}
|
||||
def ratio(float) when is_float(float) and float == 0.0, do: {0, 1}
|
||||
|
||||
def ratio(float) when is_float(float) do
|
||||
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
|
||||
|
||||
@@ -19,7 +19,7 @@ defmodule GenEvent do
|
||||
One alternative to GenEvent is a very minimal solution consisting of using a
|
||||
supervisor and multiple GenServers started under it. The supervisor acts as
|
||||
the "event manager" and the children GenServers act as the "event handlers".
|
||||
This approach has some shortcomings (it provides no backpressure for example)
|
||||
This approach has some shortcomings (it provides no back-pressure for example)
|
||||
but can still replace GenEvent for low-profile usages of it. [This blog post
|
||||
by José
|
||||
Valim](http://blog.plataformatec.com.br/2016/11/replacing-genevent-by-a-supervisor-genserver/)
|
||||
@@ -31,7 +31,7 @@ defmodule GenEvent do
|
||||
[GenStage](https://github.com/elixir-lang/gen_stage) provides a great
|
||||
alternative. GenStage is an external Elixir library maintained by the Elixir
|
||||
team; it provides a tool to implement systems that exchange events in a
|
||||
demand-driven way with built-in support for backpressure. See the [GenStage
|
||||
demand-driven way with built-in support for back-pressure. See the [GenStage
|
||||
documentation](https://hexdocs.pm/gen_stage) for more information.
|
||||
|
||||
### `:gen_event`
|
||||
|
||||
@@ -8,6 +8,13 @@ defmodule GenServer do
|
||||
will have a standard set of interface functions and include functionality for
|
||||
tracing and error reporting. It will also fit into a supervision tree.
|
||||
|
||||
```mermaid
|
||||
graph BT
|
||||
C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1)
|
||||
A & B & C -->|request| GenServer
|
||||
GenServer -.->|reply| A & B & C
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
The GenServer behaviour abstracts the common client-server interaction.
|
||||
@@ -136,11 +143,44 @@ defmodule GenServer do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
In practice, it is common to have both server and client functions in
|
||||
the same module. If the server and/or client implementations are growing
|
||||
complex, you may want to have them in different modules.
|
||||
|
||||
The following diagram summarizes the interactions between client and server.
|
||||
Both Client and Server are processes and communication happens via messages
|
||||
(continuous line). The Server <-> Module interaction happens when the
|
||||
GenServer process calls your code (dotted lines):
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client (Process)
|
||||
participant S as Server (Process)
|
||||
participant M as Module (Code)
|
||||
|
||||
note right of C: Typically started by a supervisor
|
||||
C->>+S: GenServer.start_link(module, arg, options)
|
||||
S-->>+M: init(arg)
|
||||
M-->>-S: {:ok, state} | :ignore | {:error, reason}
|
||||
S->>-C: {:ok, pid} | :ignore | {:error, reason}
|
||||
|
||||
note right of C: call is synchronous
|
||||
C->>+S: GenServer.call(pid, message)
|
||||
S-->>+M: handle_call(message, from, state)
|
||||
M-->>-S: {:reply, reply, state} | {:stop, reason, reply, state}
|
||||
S->>-C: reply
|
||||
|
||||
note right of C: cast is asynchronous
|
||||
C-)S: GenServer.cast(pid, message)
|
||||
S-->>+M: handle_cast(message, state)
|
||||
M-->>-S: {:noreply, state} | {:stop, reason, state}
|
||||
|
||||
note right of C: send is asynchronous
|
||||
C-)S: Kernel.send(pid, message)
|
||||
S-->>+M: handle_info(message, state)
|
||||
M-->>-S: {:noreply, state} | {:stop, reason, state}
|
||||
```
|
||||
|
||||
## How to supervise
|
||||
|
||||
A `GenServer` is most commonly started under a supervision tree.
|
||||
@@ -431,7 +471,7 @@ defmodule GenServer do
|
||||
guide provides a tutorial-like introduction. The documentation and links
|
||||
in Erlang can also provide extra insight.
|
||||
|
||||
* [GenServer - Elixir's Getting Started Guide](https://elixir-lang.org/getting-started/mix-otp/genserver.html)
|
||||
* [GenServer - Elixir's Getting Started Guide](genservers.md)
|
||||
* [`:gen_server` module documentation](`:gen_server`)
|
||||
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
|
||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
|
||||
|
||||
@@ -117,13 +117,22 @@ defprotocol Inspect do
|
||||
|
||||
In case there is an error while your structure is being inspected,
|
||||
Elixir will raise an `ArgumentError` error and will automatically fall back
|
||||
to a raw representation for printing the structure.
|
||||
to a raw representation for printing the structure. Furthermore, you
|
||||
must be careful when debugging your own Inspect implementation, as calls
|
||||
to `IO.inspect/2` or `dbg/1` may trigger an infinite loop (as in order to
|
||||
inspect/debug the data structure, you must call `inspect` itself).
|
||||
|
||||
You can, however, access the underlying error by invoking the `Inspect`
|
||||
implementation directly. For example, to test `Inspect.MapSet` above,
|
||||
you can invoke it as:
|
||||
Here are some tips:
|
||||
|
||||
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
|
||||
* For debugging, use `IO.inspect/2` with the `structs: false` option,
|
||||
which disables custom printing and avoids calling the Inspect
|
||||
implementation recursively
|
||||
|
||||
* To access the underlying error on your custom `Inspect` implementation,
|
||||
you may invoke the protocol directly. For example, we could invoke the
|
||||
`Inspect.MapSet` implementation above as:
|
||||
|
||||
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
|
||||
|
||||
"""
|
||||
|
||||
@@ -408,6 +417,7 @@ defimpl Inspect, for: Regex do
|
||||
|
||||
defp normalize(<<?\\, ?\\, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?\\, ?\\>>)
|
||||
defp normalize(<<?\\, ?/, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?/>>)
|
||||
defp normalize(<<?\\, ?#, ?{, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?#, ?{>>)
|
||||
defp normalize(<<char, rest::binary>>, acc), do: normalize(rest, <<acc::binary, char>>)
|
||||
defp normalize(<<>>, acc), do: acc
|
||||
|
||||
|
||||
@@ -143,9 +143,8 @@ defmodule Inspect.Opts do
|
||||
function as this must be controlled by applications. Libraries
|
||||
should instead define their own structs with custom inspect
|
||||
implementations. If a library must change the default inspect
|
||||
function, then it is best to define to ask users of your library
|
||||
to explicitly call `default_inspect_fun/1` with your function of
|
||||
choice.
|
||||
function, then it is best to ask users of your library to explicitly
|
||||
call `default_inspect_fun/1` with your function of choice.
|
||||
|
||||
The default is `Inspect.inspect/2`.
|
||||
|
||||
|
||||
+40
-24
@@ -268,9 +268,11 @@ defmodule IO do
|
||||
to stdio with this function will likely result in the wrong data
|
||||
being sent down the wire.
|
||||
"""
|
||||
@spec binwrite(device, iodata) :: :ok | {:error, term}
|
||||
@spec binwrite(device, iodata) :: :ok
|
||||
def binwrite(device \\ :stdio, iodata) when is_iodata(iodata) do
|
||||
:file.write(map_dev(device), iodata)
|
||||
with {:error, reason} <- :file.write(map_dev(device), iodata) do
|
||||
:erlang.error(reason)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -306,17 +308,20 @@ defmodule IO do
|
||||
entry from the compilation environment will be used
|
||||
|
||||
* a keyword list with at least the `:file` option representing
|
||||
a single stacktrace entry (since v1.14.0). The `:line`, `:module`,
|
||||
`:function` options are also supported
|
||||
a single stacktrace entry (since v1.14.0). The `:line`, `:column`,
|
||||
`:module`, and `:function` options are also supported
|
||||
|
||||
This function also notifies the compiler a warning was printed
|
||||
(in case --warnings-as-errors was enabled). It returns `:ok`
|
||||
if it succeeds.
|
||||
This function notifies the compiler a warning was printed
|
||||
and emits a compiler diagnostic (`t:Code.diagnostic/1`).
|
||||
The diagnostic will include precise file and location information
|
||||
if a `Macro.Env` is given or those values have been passed as
|
||||
keyword list, but not for stacktraces, as they are often imprecise.
|
||||
|
||||
It returns `:ok` if it succeeds.
|
||||
|
||||
## Examples
|
||||
|
||||
stacktrace = [{MyApp, :main, 1, [file: 'my_app.ex', line: 4]}]
|
||||
IO.warn("variable bar is unused", stacktrace)
|
||||
IO.warn("variable bar is unused", module: MyApp, function: {:main, 1}, line: 4, file: "my_app.ex")
|
||||
#=> warning: variable bar is unused
|
||||
#=> my_app.ex:4: MyApp.main/1
|
||||
|
||||
@@ -325,37 +330,46 @@ defmodule IO do
|
||||
:ok
|
||||
def warn(message, stacktrace_info)
|
||||
|
||||
def warn(message, %Macro.Env{} = env) do
|
||||
warn(message, Macro.Env.stacktrace(env))
|
||||
end
|
||||
def warn(message, %Macro.Env{line: line, file: file} = env) do
|
||||
message = to_chardata(message)
|
||||
|
||||
def warn(message, []) do
|
||||
:elixir_errors.emit_diagnostic(:warning, 0, nil, to_chardata(message), [])
|
||||
:elixir_errors.emit_diagnostic(:warning, line, file, message, Macro.Env.stacktrace(env),
|
||||
read_snippet: true
|
||||
)
|
||||
end
|
||||
|
||||
def warn(message, [{_, _} | _] = keyword) do
|
||||
if file = keyword[:file] do
|
||||
warn(
|
||||
message,
|
||||
%{
|
||||
line = keyword[:line]
|
||||
column = keyword[:column]
|
||||
position = if line && column, do: {line, column}, else: line
|
||||
message = to_chardata(message)
|
||||
|
||||
stacktrace =
|
||||
Macro.Env.stacktrace(%{
|
||||
__ENV__
|
||||
| module: keyword[:module],
|
||||
function: keyword[:function],
|
||||
line: keyword[:line],
|
||||
line: line,
|
||||
file: file
|
||||
}
|
||||
})
|
||||
|
||||
:elixir_errors.emit_diagnostic(:warning, position, file, message, stacktrace,
|
||||
read_snippet: true
|
||||
)
|
||||
else
|
||||
warn(message, [])
|
||||
end
|
||||
end
|
||||
|
||||
def warn(message, [{_, _, _, opts} | _] = stacktrace) do
|
||||
def warn(message, []) do
|
||||
message = to_chardata(message)
|
||||
line = opts[:line]
|
||||
file = opts[:file]
|
||||
file = file && List.to_string(file)
|
||||
:elixir_errors.emit_diagnostic(:warning, line || 0, file, message, stacktrace)
|
||||
:elixir_errors.emit_diagnostic(:warning, 0, nil, message, [], read_snippet: false)
|
||||
end
|
||||
|
||||
def warn(message, [{_, _, _, _} | _] = stacktrace) do
|
||||
message = to_chardata(message)
|
||||
:elixir_errors.emit_diagnostic(:warning, 0, nil, message, stacktrace, read_snippet: false)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -563,6 +577,7 @@ defmodule IO do
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec stream() :: Enumerable.t(String.t())
|
||||
def stream, do: stream(:stdio, :line)
|
||||
|
||||
@doc """
|
||||
@@ -616,6 +631,7 @@ defmodule IO do
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec binstream() :: Enumerable.t(binary)
|
||||
def binstream, do: binstream(:stdio, :line)
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -274,6 +274,8 @@ defmodule IO.ANSI do
|
||||
emitting actual ANSI codes. When `false`, no ANSI codes will be emitted.
|
||||
By default checks if ANSI is enabled using the `enabled?/0` function.
|
||||
|
||||
An `ArgumentError` will be raised if an invalid ANSI code is provided.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.ANSI.format(["Hello, ", :red, :bright, "world!"], true)
|
||||
@@ -315,6 +317,7 @@ defmodule IO.ANSI do
|
||||
end
|
||||
|
||||
defp do_format(term, rem, acc, false, append_reset) when is_atom(term) do
|
||||
format_sequence(term)
|
||||
do_format([], rem, acc, false, append_reset)
|
||||
end
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ defmodule IO.ANSI.Docs do
|
||||
"""
|
||||
@spec print_headings([String.t()], keyword) :: :ok
|
||||
def print_headings(headings, options \\ []) do
|
||||
# It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from
|
||||
# breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split
|
||||
# any heading containgin newline characters into multiple headings, that way each one is padded on its own.
|
||||
headings = Enum.flat_map(headings, fn heading -> String.split(heading, "\n") end)
|
||||
options = Keyword.merge(default_options(), options)
|
||||
newline_after_block(options)
|
||||
width = options[:width]
|
||||
@@ -114,196 +118,10 @@ defmodule IO.ANSI.Docs do
|
||||
print_markdown(doc, options)
|
||||
end
|
||||
|
||||
def print(doc, "application/erlang+html", options) when is_list(options) do
|
||||
print_erlang_html(doc, options)
|
||||
end
|
||||
|
||||
def print(_doc, format, options) when is_binary(format) and is_list(options) do
|
||||
IO.puts("\nUnknown documentation format #{inspect(format)}\n")
|
||||
end
|
||||
|
||||
## Erlang+html
|
||||
|
||||
def print_erlang_html(doc, options) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
IO.write(traverse_erlang_html(doc, "", options))
|
||||
end
|
||||
|
||||
defp traverse_erlang_html(text, _indent, _options) when is_binary(text) do
|
||||
text
|
||||
end
|
||||
|
||||
defp traverse_erlang_html(nodes, indent, options) when is_list(nodes) do
|
||||
for node <- nodes do
|
||||
traverse_erlang_html(node, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:div, [class: class] ++ _, entries}, indent, options) do
|
||||
prefix = indent <> quote_prefix(options)
|
||||
|
||||
content =
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing()
|
||||
|
||||
[
|
||||
prefix,
|
||||
class |> to_string() |> String.upcase(),
|
||||
"\n#{prefix}\n#{prefix}" | String.replace(content, "\n", "\n#{prefix}")
|
||||
]
|
||||
|> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:p, _, entries}, indent, options) do
|
||||
[indent | handle_erlang_html_text(entries, indent, options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h1, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(1, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h2, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(2, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h3, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(3, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h4, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(4, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h5, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(5, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h6, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(6, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:br, _, []}, _indent, _options) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:i, _, entries}, indent, options) do
|
||||
inline_text("_", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:em, _, entries}, indent, options) do
|
||||
inline_text("*", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({tag, _, entries}, indent, options) when tag in [:strong, :b] do
|
||||
inline_text("**", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:code, _, entries}, indent, options) do
|
||||
inline_text("`", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:pre, _, [{:code, _, entries}]}, indent, options) do
|
||||
string =
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
["#{indent} ", String.replace(string, "\n", "\n#{indent} ")] |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:a, attributes, entries}, indent, options) do
|
||||
if href = attributes[:href] do
|
||||
[traverse_erlang_html(entries, indent, options), ?\s, ?(, href, ?)]
|
||||
else
|
||||
traverse_erlang_html(entries, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dl, _, entries}, indent, options) do
|
||||
traverse_erlang_html(entries, indent, options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dt, _, entries}, indent, options) do
|
||||
[
|
||||
"#{indent} ",
|
||||
bullet_text(options) | handle_erlang_html_text(entries, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dd, _, entries}, indent, options) do
|
||||
["#{indent} " | handle_erlang_html_text(entries, indent <> " ", options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:ul, attributes, entries}, indent, options) do
|
||||
if attributes[:class] == "types" do
|
||||
types =
|
||||
for {:li, _, lines} <- entries,
|
||||
line <- lines,
|
||||
do: ["#{indent} ", traverse_erlang_html(line, indent <> " ", options), ?\n]
|
||||
|
||||
if types != [] do
|
||||
["#{indent}Typespecs:\n\n", types, ?\n]
|
||||
else
|
||||
[]
|
||||
end
|
||||
else
|
||||
for {:li, _, lines} <- entries do
|
||||
[
|
||||
"#{indent} ",
|
||||
bullet_text(options) | handle_erlang_html_text(lines, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:ol, _, entries}, indent, options) do
|
||||
for {{:li, _, lines}, i} <- Enum.with_index(entries, 1) do
|
||||
[
|
||||
"#{indent} ",
|
||||
Integer.to_string(i),
|
||||
". " | handle_erlang_html_text(lines, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({tag, _, entries}, indent, options) do
|
||||
[
|
||||
indent <> "<#{tag}>\n",
|
||||
traverse_erlang_html(entries, indent <> " ", options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing(),
|
||||
"\n" <> indent <> "</#{tag}>"
|
||||
]
|
||||
|> newline_cons()
|
||||
end
|
||||
|
||||
defp newline_cons(text) do
|
||||
[text | "\n\n"]
|
||||
end
|
||||
|
||||
defp handle_erlang_html_text(entries, indent, options) do
|
||||
if Enum.all?(entries, &inline_html?/1) do
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.split(@spaces)
|
||||
|> wrap_text(options[:width], indent, true, "", [])
|
||||
|> tl()
|
||||
|> newline_cons()
|
||||
else
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_leading()
|
||||
end
|
||||
end
|
||||
|
||||
defp inline_html?(binary) when is_binary(binary), do: true
|
||||
defp inline_html?({tag, _, _}) when tag in [:a, :code, :em, :i, :strong, :b, :br], do: true
|
||||
defp inline_html?(_), do: false
|
||||
|
||||
## Markdown
|
||||
|
||||
def print_markdown(doc, options) do
|
||||
@@ -949,10 +767,6 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{maybe_reset(options)}"
|
||||
|
||||
defp heading(text, n, options) do
|
||||
[color(:doc_headings, options), String.duplicate("#", n), " ", text, maybe_reset(options)]
|
||||
end
|
||||
|
||||
defp inline_text(mark, text, options) do
|
||||
if options[:enabled] do
|
||||
[[color_for(mark, options) | text] | IO.ANSI.reset()]
|
||||
|
||||
+69
-99
@@ -29,7 +29,7 @@ defmodule Kernel do
|
||||
|
||||
import Kernel, except: [if: 2, unless: 2]
|
||||
|
||||
See `Kernel.SpecialForms.import/2` for more information on importing.
|
||||
See `import/2` for more information on importing.
|
||||
|
||||
Elixir also has special forms that are always imported and
|
||||
cannot be skipped. These are described in `Kernel.SpecialForms`.
|
||||
@@ -58,7 +58,7 @@ defmodule Kernel do
|
||||
|
||||
There are two data types without an accompanying module:
|
||||
|
||||
* Bitstring - a sequence of bits, created with `Kernel.SpecialForms.<<>>/1`.
|
||||
* Bitstring - a sequence of bits, created with `<<>>/1`.
|
||||
When the number of bits is divisible by 8, they are called binaries and can
|
||||
be manipulated with Erlang's `:binary` module
|
||||
* Reference - a unique value in the runtime system, created with `make_ref/0`
|
||||
@@ -124,8 +124,9 @@ defmodule Kernel do
|
||||
|
||||
### Supporting documents
|
||||
|
||||
Elixir documentation also includes supporting documents under the
|
||||
"Pages" section. Those are:
|
||||
Under the "Pages" section in sidebar you will find tutorials, guides,
|
||||
and reference documents that outline Elixir semantics and behaviours
|
||||
in more detail. Those are:
|
||||
|
||||
* [Compatibility and deprecations](compatibility-and-deprecations.md) - lists
|
||||
compatibility between every Elixir version and Erlang/OTP, release schema;
|
||||
@@ -133,14 +134,12 @@ defmodule Kernel do
|
||||
* [Library guidelines](library-guidelines.md) - general guidelines, anti-patterns,
|
||||
and rules for those writing libraries
|
||||
* [Naming conventions](naming-conventions.md) - naming conventions for Elixir code
|
||||
* [Operators](operators.md) - lists all Elixir operators and their precedences
|
||||
* [Operators reference](operators.md) - lists all Elixir operators and their precedences
|
||||
* [Patterns and guards](patterns-and-guards.md) - an introduction to patterns,
|
||||
guards, and extensions
|
||||
* [Syntax reference](syntax-reference.md) - the language syntax reference
|
||||
* [Typespecs](typespecs.md)- types and function specifications, including list of types
|
||||
* [Typespecs reference](typespecs.md)- types and function specifications, including list of types
|
||||
* [Unicode syntax](unicode-syntax.md) - outlines Elixir support for Unicode
|
||||
* [Writing documentation](writing-documentation.md) - guidelines for writing
|
||||
documentation in Elixir
|
||||
|
||||
## Guards
|
||||
|
||||
@@ -199,7 +198,7 @@ defmodule Kernel do
|
||||
|
||||
This means **comparisons in Elixir are structural**, as it has the goal
|
||||
of comparing data types as efficiently as possible to create flexible
|
||||
and perform data structures. This distinction is specially important
|
||||
and performant data structures. This distinction is specially important
|
||||
for functions that provide ordering, such as `>/2`, `</2`, `>=/2`,
|
||||
`<=/2`, `min/2`, and `max/2`. For example:
|
||||
|
||||
@@ -380,11 +379,10 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts the part of the binary starting at `start` with length `length`.
|
||||
Binaries are zero-indexed.
|
||||
Extracts the part of the binary at `start` with `size`.
|
||||
|
||||
If `start` or `length` reference in any way outside the binary, an
|
||||
`ArgumentError` exception is raised.
|
||||
If `start` or `size` reference in any way outside the binary,
|
||||
an `ArgumentError` exception is raised.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -393,13 +391,13 @@ defmodule Kernel do
|
||||
iex> binary_part("foo", 1, 2)
|
||||
"oo"
|
||||
|
||||
A negative `length` can be used to extract bytes that come *before* the byte
|
||||
A negative `size` can be used to extract bytes that come *before* the byte
|
||||
at `start`:
|
||||
|
||||
iex> binary_part("Hello", 5, -3)
|
||||
"llo"
|
||||
|
||||
An `ArgumentError` is raised when the length is outside of the binary:
|
||||
An `ArgumentError` is raised when the size is outside of the binary:
|
||||
|
||||
binary_part("Hello", 0, 10)
|
||||
** (ArgumentError) argument error
|
||||
@@ -2066,9 +2064,6 @@ defmodule Kernel do
|
||||
{var, _, nil} when is_atom(var) ->
|
||||
invalid_concat_left_argument_error(Atom.to_string(var))
|
||||
|
||||
{:^, _, [{var, _, nil}]} when is_atom(var) ->
|
||||
invalid_concat_left_argument_error("^#{Atom.to_string(var)}")
|
||||
|
||||
_ ->
|
||||
expanded_arg
|
||||
end
|
||||
@@ -2124,20 +2119,10 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
erlang_error =
|
||||
case :erlang.system_info(:otp_release) >= [?2, ?4] do
|
||||
true ->
|
||||
fn x ->
|
||||
quote do
|
||||
:erlang.error(unquote(x), :none, error_info: %{module: Exception})
|
||||
end
|
||||
end
|
||||
|
||||
false ->
|
||||
fn x ->
|
||||
quote do
|
||||
:erlang.error(unquote(x))
|
||||
end
|
||||
end
|
||||
fn x ->
|
||||
quote do
|
||||
:erlang.error(unquote(x), :none, error_info: %{module: Exception})
|
||||
end
|
||||
end
|
||||
|
||||
case message do
|
||||
@@ -2376,7 +2361,7 @@ defmodule Kernel do
|
||||
|
||||
Keys in the `Enumerable` that don't exist in the struct are automatically
|
||||
discarded. Note that keys must be atoms, as only atoms are allowed when
|
||||
defining a struct. If keys in the `Enumerable` are duplicated, the last
|
||||
defining a struct. If there are duplicate keys in the `Enumerable`, the last
|
||||
entry will be taken (same behaviour as `Map.new/1`).
|
||||
|
||||
This function is useful for dynamically creating and updating structs, as
|
||||
@@ -2495,7 +2480,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if `term` is a struct; otherwise returns `false`.
|
||||
Returns `true` if `term` is a struct; otherwise returns `false`.
|
||||
|
||||
Allowed in guard tests.
|
||||
|
||||
@@ -2531,7 +2516,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if `term` is a struct of `name`; otherwise returns `false`.
|
||||
Returns `true` if `term` is a struct of `name`; otherwise returns `false`.
|
||||
|
||||
`is_struct/2` does not check that `name` exists and is a valid struct.
|
||||
If you want such validations, you must pattern match on the struct
|
||||
@@ -2579,7 +2564,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if `term` is an exception; otherwise returns `false`.
|
||||
Returns `true` if `term` is an exception; otherwise returns `false`.
|
||||
|
||||
Allowed in guard tests.
|
||||
|
||||
@@ -2617,7 +2602,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if `term` is an exception of `name`; otherwise returns `false`.
|
||||
Returns `true` if `term` is an exception of `name`; otherwise returns `false`.
|
||||
|
||||
Allowed in guard tests.
|
||||
|
||||
@@ -3637,7 +3622,7 @@ defmodule Kernel do
|
||||
defp do_at([], meta, name, function?, env) do
|
||||
IO.warn(
|
||||
"the @#{name}() notation (with parentheses) is deprecated, please use @#{name} (without parentheses) instead",
|
||||
Macro.Env.stacktrace(env)
|
||||
env
|
||||
)
|
||||
|
||||
do_at(nil, meta, name, function?, env)
|
||||
@@ -4603,9 +4588,8 @@ defmodule Kernel do
|
||||
Marks that the given variable should not be hygienized.
|
||||
|
||||
This macro expects a variable and it is typically invoked
|
||||
inside `Kernel.SpecialForms.quote/2` to mark that a variable
|
||||
should not be hygienized. See `Kernel.SpecialForms.quote/2`
|
||||
for more information.
|
||||
inside `quote/2` to mark that a variable
|
||||
should not be hygienized. See `quote/2` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -4641,7 +4625,7 @@ defmodule Kernel do
|
||||
be hygienized. This means the alias will be expanded when
|
||||
the macro is expanded.
|
||||
|
||||
Check `Kernel.SpecialForms.quote/2` for more information.
|
||||
Check `quote/2` for more information.
|
||||
"""
|
||||
defmacro alias!(alias) when is_atom(alias) do
|
||||
alias
|
||||
@@ -4824,6 +4808,16 @@ defmodule Kernel do
|
||||
Number.two()
|
||||
#=> 2
|
||||
|
||||
## Module names and aliases
|
||||
|
||||
Module names (and aliases) must start with an ASCII uppercase character which
|
||||
may be followed by any ASCII letter, number, or underscore. Elixir's
|
||||
[Naming Conventions](naming-conventions.md) suggest for module names and aliases
|
||||
to be written in the `CamelCase` format.
|
||||
|
||||
You can also use atoms as the module name, although they must only contain ASCII
|
||||
characters.
|
||||
|
||||
## Nesting
|
||||
|
||||
Nesting a module inside another module affects the name of the nested module:
|
||||
@@ -4842,7 +4836,7 @@ defmodule Kernel do
|
||||
If the `Foo.Bar` module is moved somewhere else, the references to `Bar` in
|
||||
the `Foo` module need to be updated to the fully-qualified name (`Foo.Bar`) or
|
||||
an alias has to be explicitly set in the `Foo` module with the help of
|
||||
`Kernel.SpecialForms.alias/2`.
|
||||
`alias/2`.
|
||||
|
||||
defmodule Foo.Bar do
|
||||
# code
|
||||
@@ -4858,7 +4852,7 @@ defmodule Kernel do
|
||||
Elixir module names can be dynamically generated. This is very
|
||||
useful when working with macros. For instance, one could write:
|
||||
|
||||
defmodule String.to_atom("Foo#{1}") do
|
||||
defmodule Module.concat(["Foo", "Bar"]) do
|
||||
# contents ...
|
||||
end
|
||||
|
||||
@@ -4894,10 +4888,10 @@ defmodule Kernel do
|
||||
{expanded, with_alias} =
|
||||
case is_atom(expanded) do
|
||||
true ->
|
||||
{full, old, opts} = alias_defmodule(alias, expanded, env)
|
||||
# Expand the module considering the current environment/nesting
|
||||
{full, old, new} = alias_defmodule(alias, expanded, env)
|
||||
meta = [defined: full, context: env.module] ++ alias_meta(alias)
|
||||
{full, {:alias, meta, [old, [as: new, warn: false]]}}
|
||||
meta = [defined: full] ++ alias_meta(alias)
|
||||
{full, {:require, meta, [old, opts]}}
|
||||
|
||||
false ->
|
||||
{expanded, nil}
|
||||
@@ -4960,26 +4954,26 @@ defmodule Kernel do
|
||||
|
||||
# defmodule Elixir.Alias
|
||||
defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, module, _env),
|
||||
do: {module, module, nil}
|
||||
do: {module, module, []}
|
||||
|
||||
# defmodule Alias in root
|
||||
defp alias_defmodule({:__aliases__, _, _}, module, %{module: nil}),
|
||||
do: {module, module, nil}
|
||||
defp alias_defmodule({:__aliases__, _, _}, module, %{module: nil}), do: {module, module, []}
|
||||
|
||||
# defmodule Alias nested
|
||||
defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do
|
||||
module = :elixir_aliases.concat([env.module, h])
|
||||
alias = String.to_atom("Elixir." <> Atom.to_string(h))
|
||||
opts = [as: alias, warn: false]
|
||||
|
||||
case t do
|
||||
[] -> {module, module, alias}
|
||||
_ -> {String.to_atom(Enum.join([module | t], ".")), module, alias}
|
||||
[] -> {module, module, opts}
|
||||
_ -> {String.to_atom(Enum.join([module | t], ".")), module, opts}
|
||||
end
|
||||
end
|
||||
|
||||
# defmodule _
|
||||
defp alias_defmodule(_raw, module, _env) do
|
||||
{module, module, nil}
|
||||
{module, module, []}
|
||||
end
|
||||
|
||||
defp module_var({name, kind}, meta) when is_atom(kind), do: {name, meta, kind}
|
||||
@@ -5070,34 +5064,18 @@ defmodule Kernel do
|
||||
* can be given more than once
|
||||
* ordered, as specified by the developer
|
||||
|
||||
## Function and variable names
|
||||
## Function names
|
||||
|
||||
Function and variable names have the following syntax:
|
||||
A _lowercase ASCII letter_ or an _underscore_, followed by any number of
|
||||
_lowercase or uppercase ASCII letters_, _numbers_, or _underscores_.
|
||||
Optionally they can end in either an _exclamation mark_ or a _question mark_.
|
||||
|
||||
For variables, any identifier starting with an underscore should indicate an
|
||||
unused variable. For example:
|
||||
|
||||
def foo(bar) do
|
||||
[]
|
||||
end
|
||||
#=> warning: variable bar is unused
|
||||
|
||||
def foo(_bar) do
|
||||
[]
|
||||
end
|
||||
#=> no warning
|
||||
|
||||
def foo(_bar) do
|
||||
_bar
|
||||
end
|
||||
#=> warning: the underscored variable "_bar" is used after being set
|
||||
Function and variable names in Elixir must start with an underscore or a
|
||||
Unicode letter that is not in uppercase or titlecase. They may continue
|
||||
using a sequence of Unicode letters, numbers, and underscores. They may
|
||||
end in `?` or `!`. Elixir's [Naming Conventions](naming-conventions.md)
|
||||
suggest for function and variable names to be written in the `snake_case`
|
||||
format.
|
||||
|
||||
## `rescue`/`catch`/`after`/`else`
|
||||
|
||||
Function bodies support `rescue`, `catch`, `after`, and `else` as `Kernel.SpecialForms.try/1`
|
||||
Function bodies support `rescue`, `catch`, `after`, and `else` as `try/1`
|
||||
does (known as "implicit try"). For example, the following two functions are equivalent:
|
||||
|
||||
def convert(number) do
|
||||
@@ -5228,7 +5206,7 @@ defmodule Kernel do
|
||||
A struct is a tagged map that allows developers to provide
|
||||
default values for keys, tags to be used in polymorphic
|
||||
dispatches and compile time assertions. For more information
|
||||
about structs, please check `Kernel.SpecialForms.%/2`.
|
||||
about structs, please check `%/2`.
|
||||
|
||||
It is only possible to define a struct per module, as the
|
||||
struct is tied to the module itself. Calling `defstruct/1`
|
||||
@@ -5353,7 +5331,8 @@ defmodule Kernel do
|
||||
"""
|
||||
defmacro defstruct(fields) do
|
||||
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
|
||||
{struct, derive, kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?)
|
||||
{struct, derive, kv, body} =
|
||||
Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)
|
||||
|
||||
case derive do
|
||||
[] -> :ok
|
||||
@@ -6030,7 +6009,7 @@ defmodule Kernel do
|
||||
@doc since: "1.14.0"
|
||||
defmacro dbg(code \\ quote(do: binding()), options \\ []) do
|
||||
{mod, fun, args} = Application.compile_env!(__CALLER__, :elixir, :dbg_callback)
|
||||
apply(mod, fun, [code, options, __CALLER__ | args])
|
||||
Macro.compile_apply(mod, fun, [code, options, __CALLER__ | args], __CALLER__)
|
||||
end
|
||||
|
||||
## Sigils
|
||||
@@ -6196,35 +6175,26 @@ defmodule Kernel do
|
||||
defmacro sigil_r(term, modifiers)
|
||||
|
||||
defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do
|
||||
binary = :elixir_interpolation.unescape_string(string, &Regex.unescape_map/1)
|
||||
binary = :elixir_interpolation.unescape_string(string, ®ex_unescape_map/1)
|
||||
regex = Regex.compile!(binary, :binary.list_to_bin(options))
|
||||
Macro.escape(regex)
|
||||
end
|
||||
|
||||
defmacro sigil_r({:<<>>, meta, pieces}, options) do
|
||||
binary = {:<<>>, meta, unescape_tokens(pieces, &Regex.unescape_map/1)}
|
||||
binary = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)}
|
||||
quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options))))
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Handles the sigil `~R` for regular expressions.
|
||||
|
||||
It returns a regular expression pattern without interpolations and
|
||||
without escape characters. Note it still supports escape of Regex
|
||||
tokens (such as escaping `+` or `?`) and it also requires you to
|
||||
escape the closing sigil character itself if it appears on the Regex.
|
||||
|
||||
More information on regexes can be found in the `Regex` module.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.match?(~R(f#{1,3}o), "f#o")
|
||||
true
|
||||
|
||||
"""
|
||||
defmacro sigil_R(term, modifiers)
|
||||
defp regex_unescape_map(:newline), do: true
|
||||
defp regex_unescape_map(_), do: false
|
||||
|
||||
@doc false
|
||||
defmacro sigil_R({:<<>>, _meta, [string]}, options) when is_binary(string) do
|
||||
IO.warn(
|
||||
"~R/.../ is deprecated, use ~r/.../ instead",
|
||||
Macro.Env.stacktrace(__CALLER__)
|
||||
)
|
||||
|
||||
regex = Regex.compile!(string, :binary.list_to_bin(options))
|
||||
Macro.escape(regex)
|
||||
end
|
||||
|
||||
@@ -14,19 +14,12 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
@doc """
|
||||
Starts a task for parallel compilation.
|
||||
|
||||
If you have a file that needs to compile other modules in parallel,
|
||||
the spawned processes need to be aware of the compiler environment.
|
||||
This function allows a developer to create a task that is aware of
|
||||
those environments.
|
||||
|
||||
See `Task.async/1` for more information. The task spawned must be
|
||||
always awaited on by calling `Task.await/1`
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
# TODO: Deprecate this on Elixir v1.20.
|
||||
@doc deprecated: "Use `pmap/2` instead"
|
||||
def async(fun) when is_function(fun, 0) do
|
||||
case :erlang.get(:elixir_compiler_info) do
|
||||
{compiler, _} ->
|
||||
{compiler_pid, file_pid} ->
|
||||
file = :erlang.get(:elixir_compiler_file)
|
||||
dest = :erlang.get(:elixir_compiler_dest)
|
||||
|
||||
@@ -34,9 +27,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
{_parent, checker} = Module.ParallelChecker.get()
|
||||
|
||||
Task.async(fn ->
|
||||
send(compiler, {:async, self()})
|
||||
Module.ParallelChecker.put(compiler, checker)
|
||||
:erlang.put(:elixir_compiler_info, {compiler, self()})
|
||||
send(compiler_pid, {:async, self()})
|
||||
Module.ParallelChecker.put(compiler_pid, checker)
|
||||
:erlang.put(:elixir_compiler_info, {compiler_pid, file_pid})
|
||||
:erlang.put(:elixir_compiler_file, file)
|
||||
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
|
||||
:erlang.process_flag(:error_handler, error_handler)
|
||||
@@ -50,6 +43,64 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Perform parallel compilation of `collection` with `fun`.
|
||||
|
||||
If you have a file that needs to compile other modules in parallel,
|
||||
the spawned processes need to be aware of the compiler environment.
|
||||
This function allows a developer to perform such tasks.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
def pmap(collection, fun) when is_function(fun, 1) do
|
||||
parent = self()
|
||||
ref = make_ref()
|
||||
|
||||
# We spawn a series of tasks for parallel processing.
|
||||
# The tasks notify themselves to the compiler.
|
||||
tasks =
|
||||
Enum.map(collection, fn item ->
|
||||
async(fn ->
|
||||
send(parent, {ref, self()})
|
||||
|
||||
receive do
|
||||
^ref -> fun.(item)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
# Then the tasks notify us. This is important because if
|
||||
# we wait before the tasks notify the compiler, we may be
|
||||
# released as there is nothing else running.
|
||||
on =
|
||||
for %{pid: pid} <- tasks do
|
||||
receive do
|
||||
{^ref, ^pid} -> pid
|
||||
end
|
||||
end
|
||||
|
||||
# Notify the compiler we are waiting on the tasks.
|
||||
{compiler_pid, file_pid} = :erlang.get(:elixir_compiler_info)
|
||||
defining = :elixir_module.compiler_modules()
|
||||
send(compiler_pid, {:waiting, :pmap, self(), ref, file_pid, on, defining, :raise})
|
||||
|
||||
# Now we allow the tasks to run. This step is not strictly
|
||||
# necessary but it makes compilation more deterministic by
|
||||
# only allowing tasks to run once we are waiting.
|
||||
Enum.each(on, &send(&1, ref))
|
||||
|
||||
# Await tasks and notify the compiler they are done. We could
|
||||
# have the tasks report directly to the compiler, which in turn
|
||||
# would notify us, but that would require reimplementing await_many,
|
||||
# and copying the results across boundaries, so we don't.
|
||||
res = Task.await_many(tasks, :infinity)
|
||||
send(compiler_pid, {:available, :pmap, on})
|
||||
|
||||
# Only run once the compiler lets us, to avoid unbounded parallelism.
|
||||
receive do
|
||||
{^ref, _result} -> res
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles the given files.
|
||||
|
||||
@@ -404,7 +455,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
# No more queue, nothing waiting, this cycle is done
|
||||
defp spawn_workers([], spawned, waiting, files, result, warnings, errors, state)
|
||||
when map_size(spawned) == 0 and map_size(waiting) == 0 do
|
||||
[] = errors
|
||||
# Print any spurious error that we may have found
|
||||
Enum.map(errors, fn {diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
end)
|
||||
|
||||
[] = files
|
||||
cycle_return = each_cycle_return(state.each_cycle.())
|
||||
state = cycle_timing(result, state)
|
||||
@@ -459,8 +514,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
if deadlocked do
|
||||
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, errors, state)
|
||||
else
|
||||
deadlock_errors = handle_deadlock(waiting, files)
|
||||
{return_error(deadlock_errors ++ errors, warnings), state}
|
||||
return_error(warnings, errors, state, fn ->
|
||||
handle_deadlock(waiting, files)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -528,7 +584,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
nilify_empty_or_sort(
|
||||
for %{pid: file_pid} <- files,
|
||||
{pid, {_, _, ^file_pid, on, _, _}} <- waiting_list,
|
||||
not defining?(on, waiting_list),
|
||||
is_atom(on) and not defining?(on, waiting_list),
|
||||
do: {pid, :not_found}
|
||||
)
|
||||
end
|
||||
@@ -536,7 +592,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
defp deadlocked(waiting_list, type, defining?) do
|
||||
nilify_empty_or_sort(
|
||||
for {pid, {_, _, _, on, _, ^type}} <- waiting_list,
|
||||
defining?(on, waiting_list) == defining?,
|
||||
is_atom(on) and defining?(on, waiting_list) == defining?,
|
||||
do: {pid, :deadlock}
|
||||
)
|
||||
end
|
||||
@@ -558,8 +614,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
new_spawned = Map.put(spawned, ref, pid)
|
||||
wait_for_messages(queue, new_spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
{:available, kind, module} ->
|
||||
{available, result} = update_result(result, kind, module, true)
|
||||
{:available, kind, on} ->
|
||||
{available, result} = update_result(result, kind, on, :done)
|
||||
|
||||
spawn_workers(
|
||||
available ++ queue,
|
||||
@@ -577,7 +633,6 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
# Release the module loader which is waiting for an ack
|
||||
send(child, {ref, :ack})
|
||||
|
||||
{available, result} = update_result(result, :module, module, binary)
|
||||
|
||||
spawn_workers(
|
||||
@@ -596,7 +651,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
send(child, {ref, :not_found})
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
{:waiting, kind, child_pid, ref, file_pid, on, defining, deadlock?} ->
|
||||
{:waiting, kind, child_pid, ref, file_pid, on, defining, deadlock} ->
|
||||
# If we already got what we were waiting for, do not put it on waiting.
|
||||
# If we're waiting on ourselves, send :found so that we can crash with
|
||||
# a better error.
|
||||
@@ -607,7 +662,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
send(child_pid, {ref, :found})
|
||||
{waiting, files, result}
|
||||
else
|
||||
waiting = Map.put(waiting, child_pid, {kind, ref, file_pid, on, defining, deadlock?})
|
||||
waiting = Map.put(waiting, child_pid, {kind, ref, file_pid, on, defining, deadlock})
|
||||
files = update_timing(files, file_pid, :compiling)
|
||||
result = Map.put(result, {kind, on}, [child_pid | available_or_pending])
|
||||
{waiting, files, result}
|
||||
@@ -631,12 +686,13 @@ defmodule Kernel.ParallelCompiler do
|
||||
state = %{state | timer_ref: timer_ref}
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
{:diagnostic, %{severity: :warning} = diagnostic} ->
|
||||
warnings = [Module.ParallelChecker.format_diagnostic_file(diagnostic) | warnings]
|
||||
{:diagnostic, %{severity: :warning, file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
warnings = [%{diagnostic | file: file && Path.absname(file)} | warnings]
|
||||
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
{:diagnostic, %{severity: :error} = diagnostic} ->
|
||||
errors = [Module.ParallelChecker.format_diagnostic_file(diagnostic) | errors]
|
||||
{:diagnostic, %{severity: :error} = diagnostic, read_snippet} ->
|
||||
errors = [{diagnostic, read_snippet} | errors]
|
||||
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
{:file_ok, child_pid, ref, file, lexical} ->
|
||||
@@ -656,10 +712,13 @@ defmodule Kernel.ParallelCompiler do
|
||||
spawn_workers(queue, new_spawned, waiting, new_files, result, warnings, errors, state)
|
||||
|
||||
{:file_error, child_pid, file, {kind, reason, stack}} ->
|
||||
print_error(file, kind, reason, stack)
|
||||
{_file, _new_spawned, new_files} = discard_file_pid(spawned, files, child_pid)
|
||||
terminate(new_files)
|
||||
{return_error([to_error(file, kind, reason, stack) | errors], warnings), state}
|
||||
|
||||
return_error(warnings, errors, state, fn ->
|
||||
print_error(file, kind, reason, stack)
|
||||
[to_error(file, kind, reason, stack)]
|
||||
end)
|
||||
|
||||
{:DOWN, ref, :process, pid, reason} when is_map_key(spawned, ref) ->
|
||||
# async spawned processes have no file, so we always have to delete the ref directly
|
||||
@@ -668,18 +727,27 @@ defmodule Kernel.ParallelCompiler do
|
||||
{file, spawned, files} = discard_file_pid(spawned, files, pid)
|
||||
|
||||
if file do
|
||||
print_error(file.file, :exit, reason, [])
|
||||
terminate(files)
|
||||
{return_error([to_error(file.file, :exit, reason, []) | errors], warnings), state}
|
||||
|
||||
return_error(warnings, errors, state, fn ->
|
||||
print_error(file.file, :exit, reason, [])
|
||||
[to_error(file.file, :exit, reason, [])]
|
||||
end)
|
||||
else
|
||||
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp return_error(errors, warnings) do
|
||||
defp return_error(warnings, errors, state, fun) do
|
||||
errors =
|
||||
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
%{diagnostic | file: file && Path.absname(file)}
|
||||
end)
|
||||
|
||||
info = %{compile_warnings: Enum.reverse(warnings), runtime_warnings: []}
|
||||
{:error, Enum.reverse(errors), info}
|
||||
{{:error, Enum.reverse(errors, fun.()), info}, state}
|
||||
end
|
||||
|
||||
defp update_result(result, kind, module, value) do
|
||||
@@ -809,12 +877,16 @@ defmodule Kernel.ParallelCompiler do
|
||||
)
|
||||
|
||||
for {file, _, description, stacktrace} <- deadlock do
|
||||
file = Path.absname(file)
|
||||
|
||||
%{
|
||||
severity: :error,
|
||||
file: Path.absname(file),
|
||||
file: file,
|
||||
source: file,
|
||||
position: nil,
|
||||
message: description,
|
||||
stacktrace: stacktrace
|
||||
stacktrace: stacktrace,
|
||||
span: nil
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -838,42 +910,64 @@ defmodule Kernel.ParallelCompiler do
|
||||
])
|
||||
end
|
||||
|
||||
defp to_error(file, kind, reason, stack) do
|
||||
line = get_line(file, reason, stack)
|
||||
file = Path.absname(file)
|
||||
defp to_error(source, kind, reason, stack) do
|
||||
{file, line, span} = get_snippet_info(source, reason, stack)
|
||||
source = Path.absname(source)
|
||||
message = :unicode.characters_to_binary(Kernel.CLI.format_error(kind, reason, stack))
|
||||
%{file: file, position: line || 0, message: message, severity: :error, stacktrace: stack}
|
||||
|
||||
%{
|
||||
file: file || source,
|
||||
source: source,
|
||||
position: line || 0,
|
||||
message: message,
|
||||
severity: :error,
|
||||
stacktrace: stack,
|
||||
span: span,
|
||||
details: {kind, reason}
|
||||
}
|
||||
end
|
||||
|
||||
defp get_line(_file, %{line: line, column: column}, _stack)
|
||||
defp get_snippet_info(
|
||||
_file,
|
||||
%{file: file, line: line, column: column, end_line: end_line, end_column: end_column},
|
||||
_stack
|
||||
)
|
||||
when is_integer(line) and line > 0 and is_integer(column) and column >= 0 and
|
||||
is_integer(end_line) and end_line > 0 and is_integer(end_column) and end_column >= 0 do
|
||||
{Path.absname(file), {line, column}, {end_line, end_column}}
|
||||
end
|
||||
|
||||
defp get_snippet_info(_file, %{file: file, line: line, column: column}, _stack)
|
||||
when is_integer(line) and line > 0 and is_integer(column) and column >= 0 do
|
||||
{line, column}
|
||||
{Path.absname(file), {line, column}, nil}
|
||||
end
|
||||
|
||||
defp get_line(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do
|
||||
line
|
||||
defp get_snippet_info(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do
|
||||
{nil, line, nil}
|
||||
end
|
||||
|
||||
defp get_line(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do
|
||||
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
|
||||
Keyword.get(info, :line)
|
||||
end
|
||||
defp get_snippet_info(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do
|
||||
get_snippet_info_from_stacktrace_info(info, file)
|
||||
end
|
||||
|
||||
defp get_line(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _])
|
||||
defp get_snippet_info(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _])
|
||||
when expanding in [~c"expanding macro", ~c"expanding struct"] do
|
||||
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
|
||||
Keyword.get(info, :line)
|
||||
end
|
||||
get_snippet_info_from_stacktrace_info(info, file)
|
||||
end
|
||||
|
||||
defp get_line(file, _reason, [{_, _, _, info} | _]) do
|
||||
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
|
||||
Keyword.get(info, :line)
|
||||
end
|
||||
defp get_snippet_info(file, _reason, [{_, _, _, info} | _]) do
|
||||
get_snippet_info_from_stacktrace_info(info, file)
|
||||
end
|
||||
|
||||
defp get_line(_, _, _) do
|
||||
nil
|
||||
defp get_snippet_info(_, _, _) do
|
||||
{nil, nil, nil}
|
||||
end
|
||||
|
||||
defp get_snippet_info_from_stacktrace_info(info, file) do
|
||||
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
|
||||
{nil, Keyword.get(info, :line), nil}
|
||||
else
|
||||
{nil, nil, nil}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,20 +187,23 @@ defmodule Kernel.SpecialForms do
|
||||
iex> <<0, "foo">>
|
||||
<<0, 102, 111, 111>>
|
||||
|
||||
Binaries need to be explicitly tagged as `binary`:
|
||||
You can use one of `utf8` (the default), `utf16`, and `utf32` to
|
||||
control how the string is encoded:
|
||||
|
||||
iex> <<"foo"::utf16>>
|
||||
<<0, 102, 0, 111, 0, 111>>
|
||||
|
||||
Which is equivalent to writing:
|
||||
|
||||
iex> <<?f::utf16, ?o::utf16, ?o::utf16>>
|
||||
<<0, 102, 0, 111, 0, 111>>
|
||||
|
||||
At runtime, binaries need to be explicitly tagged as `binary`:
|
||||
|
||||
iex> rest = "oo"
|
||||
iex> <<102, rest::binary>>
|
||||
"foo"
|
||||
|
||||
The `utf8`, `utf16`, and `utf32` types are for Unicode code points. They
|
||||
can also be applied to literal strings and charlists:
|
||||
|
||||
iex> <<"foo"::utf16>>
|
||||
<<0, 102, 0, 111, 0, 111>>
|
||||
iex> <<"foo"::utf32>>
|
||||
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>
|
||||
|
||||
Otherwise we get an `ArgumentError` when constructing the binary:
|
||||
|
||||
rest = "oo"
|
||||
@@ -294,7 +297,7 @@ defmodule Kernel.SpecialForms do
|
||||
`unsigned` (default) | `integer`
|
||||
`little` | `integer`, `float`, `utf16`, `utf32`
|
||||
`big` (default) | `integer`, `float`, `utf16`, `utf32`
|
||||
`native` | `integer`, `utf16`, `utf32`
|
||||
`native` | `integer`, `float`, `utf16`, `utf32`
|
||||
|
||||
### Sign
|
||||
|
||||
@@ -346,7 +349,7 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
Or as a part of function definitions to pattern match:
|
||||
|
||||
defmodule ImageTyper do
|
||||
defmodule ImageType do
|
||||
@png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
|
||||
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
|
||||
@jpg_signature <<255::size(8), 216::size(8)>>
|
||||
@@ -1023,9 +1026,10 @@ defmodule Kernel.SpecialForms do
|
||||
end
|
||||
end
|
||||
|
||||
require Hygiene
|
||||
Hygiene.write()
|
||||
Hygiene.read()
|
||||
** (RuntimeError) undefined variable a or undefined function a/0
|
||||
** (CompileError) undefined variable "a" (context Hygiene)
|
||||
|
||||
For such, you can explicitly pass the current module scope as
|
||||
argument:
|
||||
@@ -1044,6 +1048,7 @@ defmodule Kernel.SpecialForms do
|
||||
end
|
||||
end
|
||||
|
||||
require Hygiene
|
||||
ContextHygiene.write()
|
||||
ContextHygiene.read()
|
||||
#=> 1
|
||||
@@ -1540,7 +1545,7 @@ defmodule Kernel.SpecialForms do
|
||||
area, as `{:ok, area}` or return `:error`. We could implement
|
||||
this function as:
|
||||
|
||||
def area(map) do
|
||||
def area(opts) do
|
||||
case Map.fetch(opts, :width) do
|
||||
{:ok, width} ->
|
||||
case Map.fetch(opts, :height) do
|
||||
@@ -1559,7 +1564,7 @@ defmodule Kernel.SpecialForms do
|
||||
While the code above works, it is quite verbose. Using `with`,
|
||||
we could rewrite it as:
|
||||
|
||||
def area(map) do
|
||||
def area(opts) do
|
||||
with {:ok, width} <- Map.fetch(opts, :width),
|
||||
{:ok, height} <- Map.fetch(opts, :height) do
|
||||
{:ok, width * height}
|
||||
|
||||
@@ -385,17 +385,17 @@ defmodule Kernel.Typespec do
|
||||
compile_error(caller, error)
|
||||
end
|
||||
|
||||
line = line(meta)
|
||||
location = location(meta)
|
||||
vars = Keyword.keys(guard)
|
||||
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
|
||||
{return, state} = typespec(return, vars, caller, state)
|
||||
spec = {:type, line, :fun, [{:type, line, :product, args}, return]}
|
||||
spec = {:type, location, :fun, [{:type, location, :product, args}, return]}
|
||||
|
||||
{spec, state} =
|
||||
case guard_to_constraints(guard, vars, meta, caller, state) do
|
||||
{[], state} -> {spec, state}
|
||||
{constraints, state} -> {{:type, line, :bounded_fun, [spec, constraints]}, state}
|
||||
{constraints, state} -> {{:type, location, :bounded_fun, [spec, constraints]}, state}
|
||||
end
|
||||
|
||||
ensure_no_unused_local_vars!(caller, state.local_vars)
|
||||
@@ -437,7 +437,7 @@ defmodule Kernel.Typespec do
|
||||
defp ensure_not_default(_), do: :ok
|
||||
|
||||
defp guard_to_constraints(guard, vars, meta, caller, state) do
|
||||
line = line(meta)
|
||||
location = location(meta)
|
||||
|
||||
fun = fn
|
||||
{_name, {:var, _, context}}, {constraints, state} when is_atom(context) ->
|
||||
@@ -445,9 +445,9 @@ defmodule Kernel.Typespec do
|
||||
|
||||
{name, type}, {constraints, state} ->
|
||||
{spec, state} = typespec(type, vars, caller, state)
|
||||
constraint = [{:atom, line, :is_subtype}, [{:var, line, name}, spec]]
|
||||
constraint = [{:atom, location, :is_subtype}, [{:var, location, name}, spec]]
|
||||
state = update_local_vars(state, name)
|
||||
{[{:type, line, :constraint, constraint} | constraints], state}
|
||||
{[{:type, location, :constraint, constraint} | constraints], state}
|
||||
end
|
||||
|
||||
{constraints, state} = :lists.foldl(fun, {[], state}, guard)
|
||||
@@ -456,21 +456,27 @@ defmodule Kernel.Typespec do
|
||||
|
||||
## To typespec conversion
|
||||
|
||||
defp line(meta) do
|
||||
Keyword.get(meta, :line, 0)
|
||||
defp location(meta) do
|
||||
line = Keyword.get(meta, :line, 0)
|
||||
|
||||
if column = Keyword.get(meta, :column) do
|
||||
{line, column}
|
||||
else
|
||||
line
|
||||
end
|
||||
end
|
||||
|
||||
# Handle unions
|
||||
defp typespec({:|, meta, [_, _]} = exprs, vars, caller, state) do
|
||||
exprs = collect_union(exprs)
|
||||
{union, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, exprs)
|
||||
{{:type, line(meta), :union, union}, state}
|
||||
{{:type, location(meta), :union, union}, state}
|
||||
end
|
||||
|
||||
# Handle binaries
|
||||
defp typespec({:<<>>, meta, []}, _, _, state) do
|
||||
line = line(meta)
|
||||
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line, 0}]}, state}
|
||||
location = location(meta)
|
||||
{{:type, location, :binary, [{:integer, location, 0}, {:integer, location, 0}]}, state}
|
||||
end
|
||||
|
||||
defp typespec(
|
||||
@@ -480,14 +486,18 @@ defmodule Kernel.Typespec do
|
||||
state
|
||||
)
|
||||
when is_atom(ctx1) and is_atom(ctx2) and unit in 1..256 do
|
||||
line = line(meta)
|
||||
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line(unit_meta), unit}]}, state}
|
||||
location = location(meta)
|
||||
|
||||
{{:type, location, :binary, [{:integer, location, 0}, {:integer, location(unit_meta), unit}]},
|
||||
state}
|
||||
end
|
||||
|
||||
defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state)
|
||||
when is_atom(ctx) and is_integer(size) and size >= 0 do
|
||||
line = line(meta)
|
||||
{{:type, line, :binary, [{:integer, line(size_meta), size}, {:integer, line, 0}]}, state}
|
||||
location = location(meta)
|
||||
|
||||
{{:type, location, :binary, [{:integer, location(size_meta), size}, {:integer, location, 0}]},
|
||||
state}
|
||||
end
|
||||
|
||||
defp typespec(
|
||||
@@ -505,8 +515,8 @@ defmodule Kernel.Typespec do
|
||||
)
|
||||
when is_atom(ctx1) and is_atom(ctx2) and is_atom(ctx3) and is_integer(size) and
|
||||
size >= 0 and unit in 1..256 do
|
||||
args = [{:integer, line(size_meta), size}, {:integer, line(unit_meta), unit}]
|
||||
{{:type, line(meta), :binary, args}, state}
|
||||
args = [{:integer, location(size_meta), size}, {:integer, location(unit_meta), unit}]
|
||||
{{:type, location(meta), :binary, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({:<<>>, _meta, _args}, _vars, caller, _state) do
|
||||
@@ -519,7 +529,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
## Handle maps and structs
|
||||
defp typespec({:map, meta, args}, _vars, _caller, state) when args == [] or is_atom(args) do
|
||||
{{:type, line(meta), :map, :any}, state}
|
||||
{{:type, location(meta), :map, :any}, state}
|
||||
end
|
||||
|
||||
defp typespec({:%{}, meta, fields} = map, vars, caller, state) do
|
||||
@@ -527,17 +537,17 @@ defmodule Kernel.Typespec do
|
||||
{{:required, meta2, [k]}, v}, state ->
|
||||
{arg1, state} = typespec(k, vars, caller, state)
|
||||
{arg2, state} = typespec(v, vars, caller, state)
|
||||
{{:type, line(meta2), :map_field_exact, [arg1, arg2]}, state}
|
||||
{{:type, location(meta2), :map_field_exact, [arg1, arg2]}, state}
|
||||
|
||||
{{:optional, meta2, [k]}, v}, state ->
|
||||
{arg1, state} = typespec(k, vars, caller, state)
|
||||
{arg2, state} = typespec(v, vars, caller, state)
|
||||
{{:type, line(meta2), :map_field_assoc, [arg1, arg2]}, state}
|
||||
{{:type, location(meta2), :map_field_assoc, [arg1, arg2]}, state}
|
||||
|
||||
{k, v}, state ->
|
||||
{arg1, state} = typespec(k, vars, caller, state)
|
||||
{arg2, state} = typespec(v, vars, caller, state)
|
||||
{{:type, line(meta), :map_field_exact, [arg1, arg2]}, state}
|
||||
{{:type, location(meta), :map_field_exact, [arg1, arg2]}, state}
|
||||
|
||||
{:|, _, [_, _]}, _state ->
|
||||
error =
|
||||
@@ -551,7 +561,7 @@ defmodule Kernel.Typespec do
|
||||
end
|
||||
|
||||
{fields, state} = :lists.mapfoldl(fun, state, fields)
|
||||
{{:type, line(meta), :map, fields}, state}
|
||||
{{:type, location(meta), :map, fields}, state}
|
||||
end
|
||||
|
||||
defp typespec({:%, _, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do
|
||||
@@ -644,7 +654,7 @@ defmodule Kernel.Typespec do
|
||||
{right, state} = typespec(right, vars, caller, state)
|
||||
:ok = validate_range(left, right, caller)
|
||||
|
||||
{{:type, line(meta), :range, [left, right]}, state}
|
||||
{{:type, location(meta), :range, [left, right]}, state}
|
||||
end
|
||||
|
||||
# Handle special forms
|
||||
@@ -668,7 +678,7 @@ defmodule Kernel.Typespec do
|
||||
pair -> pair
|
||||
end
|
||||
|
||||
{{:type, line(meta), :fun, fun_args}, state}
|
||||
{{:type, location(meta), :fun, fun_args}, state}
|
||||
end
|
||||
|
||||
# Handle type operator
|
||||
@@ -691,10 +701,10 @@ defmodule Kernel.Typespec do
|
||||
# This may be generating an invalid typespec but we need to generate it
|
||||
# to avoid breaking existing code that was valid but only broke Dialyzer
|
||||
{right, state} = typespec(expr, vars, caller, state)
|
||||
{{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state}
|
||||
{{:ann_type, location(meta), [{:var, location(var_meta), var_name}, right]}, state}
|
||||
|
||||
{right, state} ->
|
||||
{{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state}
|
||||
{{:ann_type, location(meta), [{:var, location(var_meta), var_name}, right]}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -723,13 +733,13 @@ defmodule Kernel.Typespec do
|
||||
{left, state} = typespec(left, vars, caller, state)
|
||||
state = %{state | undefined_type_error_enabled?: true}
|
||||
{right, state} = typespec(right, vars, caller, state)
|
||||
{{:ann_type, line(meta), [left, right]}, state}
|
||||
{{:ann_type, location(meta), [left, right]}, state}
|
||||
end
|
||||
|
||||
# Handle unary ops
|
||||
defp typespec({op, meta, [integer]}, _, _, state) when op in [:+, :-] and is_integer(integer) do
|
||||
line = line(meta)
|
||||
{{:op, line, op, {:integer, line, integer}}, state}
|
||||
location = location(meta)
|
||||
{{:op, location, op, {:integer, location, integer}}, state}
|
||||
end
|
||||
|
||||
# Handle remote calls in the form of @module_attribute.type.
|
||||
@@ -778,12 +788,12 @@ defmodule Kernel.Typespec do
|
||||
|
||||
# Handle tuples
|
||||
defp typespec({:tuple, meta, []}, _vars, _caller, state) do
|
||||
{{:type, line(meta), :tuple, :any}, state}
|
||||
{{:type, location(meta), :tuple, :any}, state}
|
||||
end
|
||||
|
||||
defp typespec({:{}, meta, t}, vars, caller, state) when is_list(t) do
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, t)
|
||||
{{:type, line(meta), :tuple, args}, state}
|
||||
{{:type, location(meta), :tuple, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({left, right}, vars, caller, state) do
|
||||
@@ -799,7 +809,7 @@ defmodule Kernel.Typespec do
|
||||
defp typespec({name, meta, atom}, vars, caller, state) when is_atom(atom) do
|
||||
if :lists.member(name, vars) do
|
||||
state = update_local_vars(state, name)
|
||||
{{:var, line(meta), name}, state}
|
||||
{{:var, location(meta), name}, state}
|
||||
else
|
||||
typespec({name, meta, []}, vars, caller, state)
|
||||
end
|
||||
@@ -814,7 +824,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
IO.warn(warning, caller)
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
|
||||
{{:type, line(meta), :string, args}, state}
|
||||
{{:type, location(meta), :string, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({:nonempty_string, meta, args}, vars, caller, state) do
|
||||
@@ -825,7 +835,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
IO.warn(warning, caller)
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
|
||||
{{:type, line(meta), :nonempty_string, args}, state}
|
||||
{{:type, location(meta), :nonempty_string, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
@@ -855,7 +865,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
defp typespec({:fun, meta, args}, vars, caller, state) do
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
|
||||
{{:type, line(meta), :fun, args}, state}
|
||||
{{:type, location(meta), :fun, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({:..., _meta, _args}, _vars, caller, _state) do
|
||||
@@ -872,7 +882,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
case :erl_internal.is_type(name, arity) do
|
||||
true ->
|
||||
{{:type, line(meta), name, args}, state}
|
||||
{{:type, location(meta), name, args}, state}
|
||||
|
||||
false ->
|
||||
if state.undefined_type_error_enabled? and
|
||||
@@ -890,7 +900,7 @@ defmodule Kernel.Typespec do
|
||||
%{state | used_type_pairs: [{name, arity} | state.used_type_pairs]}
|
||||
end
|
||||
|
||||
{{:user_type, line(meta), name, args}, state}
|
||||
{{:user_type, location(meta), name, args}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -963,7 +973,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
defp remote_type({remote, meta, name, args}, vars, caller, state) do
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
|
||||
{{:remote_type, line(meta), [remote, name, args]}, state}
|
||||
{{:remote_type, location(meta), [remote, name, args]}, state}
|
||||
end
|
||||
|
||||
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
|
||||
@@ -996,16 +1006,16 @@ defmodule Kernel.Typespec do
|
||||
end
|
||||
|
||||
defp fn_args(meta, [{:..., _, _}], _vars, _caller, state) do
|
||||
{{:type, line(meta), :any}, state}
|
||||
{{:type, location(meta), :any}, state}
|
||||
end
|
||||
|
||||
defp fn_args(meta, args, vars, caller, state) do
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
|
||||
{{:type, line(meta), :product, args}, state}
|
||||
{{:type, location(meta), :product, args}, state}
|
||||
end
|
||||
|
||||
defp variable({name, meta, args}) when is_atom(name) and is_atom(args) do
|
||||
{:var, line(meta), name}
|
||||
{:var, location(meta), name}
|
||||
end
|
||||
|
||||
defp variable(expr), do: expr
|
||||
|
||||
@@ -36,14 +36,14 @@ defmodule Kernel.Utils do
|
||||
if is_list(funs) do
|
||||
IO.warn(
|
||||
"passing a list to Kernel.defdelegate/2 is deprecated, please define each delegate separately",
|
||||
Macro.Env.stacktrace(env)
|
||||
env
|
||||
)
|
||||
end
|
||||
|
||||
if Keyword.has_key?(opts, :append_first) do
|
||||
IO.warn(
|
||||
"Kernel.defdelegate/2 :append_first option is deprecated",
|
||||
Macro.Env.stacktrace(env)
|
||||
env
|
||||
)
|
||||
end
|
||||
|
||||
@@ -100,7 +100,7 @@ defmodule Kernel.Utils do
|
||||
@doc """
|
||||
Callback for defstruct.
|
||||
"""
|
||||
def defstruct(module, fields, bootstrapped?) do
|
||||
def defstruct(module, fields, bootstrapped?, env) do
|
||||
{set, bag} = :elixir_module.data_tables(module)
|
||||
|
||||
if :ets.member(set, :__struct__) do
|
||||
@@ -152,7 +152,7 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
|
||||
# TODO: Make it raise on v2.0
|
||||
warn_on_duplicate_struct_key(:lists.keysort(1, fields))
|
||||
warn_on_duplicate_struct_key(:lists.keysort(1, fields), env)
|
||||
|
||||
foreach = fn
|
||||
key when is_atom(key) ->
|
||||
@@ -227,17 +227,17 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_struct_key([]) do
|
||||
defp warn_on_duplicate_struct_key([], _) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_struct_key([{key, _} | [{key, _} | _] = rest]) do
|
||||
IO.warn("duplicate key #{inspect(key)} found in struct")
|
||||
warn_on_duplicate_struct_key(rest)
|
||||
defp warn_on_duplicate_struct_key([{key, _} | [{key, _} | _] = rest], env) do
|
||||
IO.warn("duplicate key #{inspect(key)} found in struct", env)
|
||||
warn_on_duplicate_struct_key(rest, env)
|
||||
end
|
||||
|
||||
defp warn_on_duplicate_struct_key([_ | rest]) do
|
||||
warn_on_duplicate_struct_key(rest)
|
||||
defp warn_on_duplicate_struct_key([_ | rest], env) do
|
||||
warn_on_duplicate_struct_key(rest, env)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -5,7 +5,9 @@ defmodule Keyword do
|
||||
The first element of these tuples is known as the *key*, and it must be an atom.
|
||||
The second element, known as the *value*, can be any term.
|
||||
|
||||
Keywords are mostly used to work with optional values.
|
||||
Keywords are mostly used to work with optional values. For a general introduction
|
||||
to keywords and how the compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
|
||||
guide.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -110,6 +112,8 @@ defmodule Keyword do
|
||||
|
||||
iex> Keyword.from_keys([:foo, :bar, :baz], :atom)
|
||||
[foo: :atom, bar: :atom, baz: :atom]
|
||||
iex> Keyword.from_keys([], :atom)
|
||||
[]
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
|
||||
@@ -57,8 +57,8 @@ defmodule List do
|
||||
iex> list ++ [4] # slow
|
||||
[1, 2, 3, 4]
|
||||
|
||||
Most of the functions in this module work in linear time. This means that,
|
||||
that the time it takes to perform an operation grows at the same rate as the
|
||||
Most of the functions in this module work in linear time. This means that
|
||||
the time it takes to perform an operation grows at the same rate as the
|
||||
length of the list. For example `length/1` and `last/1` will run in linear
|
||||
time because they need to iterate through every element of the list, but
|
||||
`first/1` will run in constant time because it only needs the first element.
|
||||
@@ -927,7 +927,7 @@ defmodule List do
|
||||
@doc """
|
||||
Converts a charlist to an atom.
|
||||
|
||||
Elixir supports conversions from charlists which contains any Unicode
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -949,7 +949,7 @@ defmodule List do
|
||||
@doc """
|
||||
Converts a charlist to an existing atom.
|
||||
|
||||
Elixir supports conversions from charlists which contains any Unicode
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point. Raises an `ArgumentError` if the atom does not exist.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -1168,7 +1168,7 @@ defmodule List do
|
||||
An *edit script* is a keyword list. Each key describes the "editing action" to
|
||||
take in order to bring `list1` closer to being equal to `list2`; a key can be
|
||||
`:eq`, `:ins`, or `:del`. Each value is a sublist of either `list1` or `list2`
|
||||
that should be inserted (if the corresponding key `:ins`), deleted (if the
|
||||
that should be inserted (if the corresponding key is `:ins`), deleted (if the
|
||||
corresponding key is `:del`), or left alone (if the corresponding key is
|
||||
`:eq`) in `list1` in order to be closer to `list2`.
|
||||
|
||||
|
||||
+45
-81
@@ -61,75 +61,6 @@ defmodule Macro do
|
||||
> The functions in this module do not evaluate code. In fact,
|
||||
> evaluating code from macros is often an anti-pattern. For code
|
||||
> evaluation, see the `Code` module.
|
||||
|
||||
## Custom Sigils
|
||||
|
||||
Macros are also commonly used to implement custom sigils.
|
||||
|
||||
Sigils start with `~` and are followed by one lowercase letter or by one
|
||||
or more uppercase letters, and then a separator
|
||||
(see the [Syntax Reference](syntax-reference.md)). One example is
|
||||
`~D[2020-10-13]` to define a date.
|
||||
|
||||
To create a custom sigil, define a macro with the name `sigil_{identifier}`
|
||||
that takes two arguments. The first argument will be the string, the second
|
||||
will be a charlist containing any modifiers. If the sigil is lower case
|
||||
(such as `sigil_x`) then the string argument will allow interpolation.
|
||||
If the sigil is one or more upper case letters (such as `sigil_X` and
|
||||
`sigil_EXAMPLE`) then the string will not be interpolated.
|
||||
|
||||
Valid modifiers are ASCII letters and digits. Any other character will
|
||||
cause a syntax error.
|
||||
|
||||
Single-letter sigils are typically reserved to the language. Multi-letter
|
||||
sigils are uppercased and extensively used by the community to embed
|
||||
alternative markups and data-types within Elixir source code.
|
||||
|
||||
The module containing the custom sigil must be imported before the sigil
|
||||
syntax can be used.
|
||||
|
||||
### Examples
|
||||
|
||||
As an example, let's define a sigil `~x` and sigil `~X` which
|
||||
return its contents as a string. However, if the `r` modifier
|
||||
is given, it reverses the string instead:
|
||||
|
||||
defmodule MySigils do
|
||||
defmacro sigil_x(term, [?r]) do
|
||||
quote do
|
||||
unquote(term) |> String.reverse()
|
||||
end
|
||||
end
|
||||
|
||||
defmacro sigil_x(term, _modifiers) do
|
||||
term
|
||||
end
|
||||
|
||||
defmacro sigil_X(term, [?r]) do
|
||||
quote do
|
||||
unquote(term) |> String.reverse()
|
||||
end
|
||||
end
|
||||
|
||||
defmacro sigil_X(term, _modifiers) do
|
||||
term
|
||||
end
|
||||
end
|
||||
|
||||
import MySigils
|
||||
|
||||
~x(with #{"inter" <> "polation"})
|
||||
#=> "with interpolation"
|
||||
|
||||
~x(with #{"inter" <> "polation"})r
|
||||
#=> "noitalopretni htiw"
|
||||
|
||||
~X(without #{"interpolation"})
|
||||
#=> "without \#{"interpolation"}"
|
||||
|
||||
~X(without #{"interpolation"})r
|
||||
#=> "}\"noitalopretni\"{# tuohtiw"
|
||||
|
||||
"""
|
||||
|
||||
alias Code.Identifier
|
||||
@@ -181,6 +112,12 @@ defmodule Macro do
|
||||
the compiler, each variable is identified by the combination of either
|
||||
`name` and `metadata[:counter]`, or `name` and `context`.
|
||||
|
||||
* `:from_brackets` - Used to determine whether a call to `Access.get/3` is from
|
||||
bracket syntax.
|
||||
|
||||
* `:from_interpolation` - Used to determine whether a call to `Kernel.to_string/1` is
|
||||
from interpolation.
|
||||
|
||||
* `:generated` - Whether the code should be considered as generated by
|
||||
the compiler or not. This means the compiler and tools like Dialyzer may not
|
||||
emit certain warnings.
|
||||
@@ -192,28 +129,37 @@ defmodule Macro do
|
||||
* `:keep` - Used by `quote/2` with the option `location: :keep` to annotate
|
||||
the file and the line number of the quoted source.
|
||||
|
||||
* `:line` - The line number of the AST node.
|
||||
|
||||
* `:from_brackets` - Used to determine whether a call to `Access.get/3` is from
|
||||
bracket syntax or a function call.
|
||||
* `:line` - The line number of the AST node. Note line information is discarded
|
||||
from quoted code but can be enabled back via the `:line` option.
|
||||
|
||||
The following metadata keys are enabled by `Code.string_to_quoted/2`:
|
||||
|
||||
* `:closing` - contains metadata about the closing pair, such as a `}`
|
||||
in a tuple or in a map, or such as the closing `)` in a function call
|
||||
with parens. The `:closing` does not delimit the end of expression if
|
||||
there are `:do` and `:end` metadata (when `:token_metadata` is true)
|
||||
* `:column` - the column number of the AST node (when `:columns` is true)
|
||||
with parens (when `:token_metadata` is true). If the function call
|
||||
has a do-end block attached to it, its metadata is found under the
|
||||
`:do` and `:end` metadata
|
||||
|
||||
* `:column` - the column number of the AST node (when `:columns` is true).
|
||||
Note column information is always discarded from quoted code.
|
||||
|
||||
* `:delimiter` - contains the opening delimiter for sigils, strings,
|
||||
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
|
||||
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword
|
||||
|
||||
* `:do` - contains metadata about the `do` location in a function call with
|
||||
`do`-`end` blocks (when `:token_metadata` is true)
|
||||
|
||||
* `:end` - contains metadata about the `end` location in a function call with
|
||||
`do`-`end` blocks (when `:token_metadata` is true)
|
||||
|
||||
* `:end_of_expression` - denotes when the end of expression effectively
|
||||
happens. Available for all expressions except the last one inside a
|
||||
`__block__` (when `:token_metadata` is true)
|
||||
happens (when `:token_metadata` is true). This is only available for
|
||||
direct children of a `__block__`, and it is either the location of a
|
||||
newline or of the `;` character. The last expression of `__block__`
|
||||
does not have this metadata.
|
||||
|
||||
* `:indentation` - indentation of a sigil heredoc
|
||||
|
||||
The following metadata keys are private:
|
||||
@@ -438,10 +384,12 @@ defmodule Macro do
|
||||
def generate_arguments(amount, context), do: generate_arguments(amount, context, &var/2)
|
||||
|
||||
@doc """
|
||||
Returns the path to the node in `ast` which `fun` returns true.
|
||||
Returns the path to the node in `ast` for which `fun` returns a truthy value.
|
||||
|
||||
The path is a list, starting with the node in which `fun` returns
|
||||
true, followed by all of its parents.
|
||||
a truthy value, followed by all of its parents.
|
||||
|
||||
Returns `nil` if `fun` returns only falsy values.
|
||||
|
||||
Computing the path can be an efficient operation when you want
|
||||
to find a particular node in the AST within its context and then
|
||||
@@ -452,6 +400,9 @@ defmodule Macro do
|
||||
iex> Macro.path(quote(do: [1, 2, 3]), & &1 == 3)
|
||||
[3, [1, 2, 3]]
|
||||
|
||||
iex> Macro.path(quote(do: [1, 2]), & &1 == 5)
|
||||
nil
|
||||
|
||||
iex> Macro.path(quote(do: Foo.bar(3)), & &1 == 3)
|
||||
[3, quote(do: Foo.bar(3))]
|
||||
|
||||
@@ -466,6 +417,7 @@ defmodule Macro do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec path(t, (t -> as_boolean(term))) :: [t] | nil
|
||||
def path(ast, fun) when is_function(fun, 1) do
|
||||
path(ast, [], fun)
|
||||
end
|
||||
@@ -1758,6 +1710,18 @@ defmodule Macro do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
|
||||
|
||||
This is used when you want to programatically invoke a macro at
|
||||
compile-time.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
def compile_apply(mod, fun, args, caller) do
|
||||
:elixir_env.trace({:remote_macro, [], mod, fun, length(args)}, caller)
|
||||
Kernel.apply(mod, fun, args)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives an AST node and expands it once.
|
||||
|
||||
@@ -2322,7 +2286,7 @@ defmodule Macro do
|
||||
## Atom handling
|
||||
|
||||
@doc """
|
||||
Classifies a runtime `atom` based on its possible AST placement.
|
||||
Classifies an `atom` based on its possible AST placement.
|
||||
|
||||
It returns one of the following atoms:
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ defmodule Macro.Env do
|
||||
following trick:
|
||||
|
||||
def make_custom_env do
|
||||
import SomeModule, only: [some_function: 2]
|
||||
alias A.B.C
|
||||
import SomeModule, only: [some_function: 2], warn: false
|
||||
alias A.B.C, warn: false
|
||||
__ENV__
|
||||
end
|
||||
|
||||
@@ -97,7 +97,7 @@ defmodule Macro.Env do
|
||||
]
|
||||
|
||||
# Define the __struct__ callbacks by hand for bootstrap reasons.
|
||||
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false)
|
||||
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
|
||||
def __struct__(), do: unquote(:elixir_quote.escape(struct, false, :none))
|
||||
def __struct__(unquote(kv)), do: unquote(body)
|
||||
|
||||
@@ -182,6 +182,8 @@ defmodule Macro.Env do
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec fetch_alias(t, atom) :: {:ok, atom} | :error
|
||||
def fetch_alias(env, atom)
|
||||
|
||||
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
|
||||
@@ -196,6 +198,8 @@ defmodule Macro.Env do
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec fetch_macro_alias(t, atom) :: {:ok, atom} | :error
|
||||
def fetch_macro_alias(env, atom)
|
||||
|
||||
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
|
||||
when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
@@ -225,6 +229,8 @@ defmodule Macro.Env do
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec lookup_import(t, name_arity) :: [{:function | :macro, module}]
|
||||
def lookup_import(env, name_arity)
|
||||
|
||||
def lookup_import(
|
||||
%{__struct__: Macro.Env, functions: functions, macros: macros},
|
||||
{name, arity} = pair
|
||||
@@ -256,12 +262,14 @@ defmodule Macro.Env do
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec lookup_alias_as(t, atom) :: [atom]
|
||||
def lookup_alias_as(env, atom)
|
||||
|
||||
def lookup_alias_as(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom) do
|
||||
for {name, ^atom} <- aliases, do: name
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the given module has been required.
|
||||
Returns `true` if the given module has been required.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -276,6 +284,8 @@ defmodule Macro.Env do
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec required?(t, module) :: boolean
|
||||
def required?(env, module)
|
||||
|
||||
def required?(%{__struct__: Macro.Env, requires: requires}, mod) when is_atom(mod),
|
||||
do: mod in requires
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ defmodule Map do
|
||||
in the example above has a different order than the map that was created).
|
||||
|
||||
Maps do not impose any restriction on the key type: anything can be a key in a
|
||||
map. As a key-value structure, maps do not allow duplicated keys. Keys are
|
||||
map. As a key-value structure, maps do not allow duplicate keys. Keys are
|
||||
compared using the exact-equality operator (`===/2`). If colliding keys are defined
|
||||
in a map literal, the last one prevails.
|
||||
|
||||
|
||||
+60
-10
@@ -283,6 +283,24 @@ defmodule Module do
|
||||
end
|
||||
end
|
||||
|
||||
Note that this is only valid for exceptions/diagnostics that come from the
|
||||
definition inner scope (which includes its patterns and guards). For example:
|
||||
|
||||
defmodule MyModule do # <---- module definition
|
||||
@file "hello.ex"
|
||||
defp unused(a) do # <---- function definition
|
||||
"world" # <---- function scope
|
||||
end
|
||||
|
||||
@file "bye.ex"
|
||||
def unused(_), do: true
|
||||
end
|
||||
|
||||
If you run this code with the second "unused" definition commented, you will
|
||||
see that `hello.ex` is used as the stacktrace when reporting warnings, but if
|
||||
you uncomment it you'll see that the error will not mention `bye.ex`, because
|
||||
it's a module-level error rather than an expression-level error.
|
||||
|
||||
### `@moduledoc`
|
||||
|
||||
Provides documentation for the current module.
|
||||
@@ -305,6 +323,21 @@ defmodule Module do
|
||||
Once this module is compiled, this information becomes available via
|
||||
the `Code.fetch_docs/1` function.
|
||||
|
||||
### `@nifs` (since v1.16.0)
|
||||
|
||||
A list of functions and their arities which will be overridden
|
||||
by a native implementation (NIF).
|
||||
|
||||
defmodule MyLibrary.MyModule do
|
||||
@nifs [foo: 1, bar: 2]
|
||||
|
||||
def foo(arg1), do: :erlang.nif_error(:not_loaded)
|
||||
def bar(arg1, arg2), do: :erlang.nif_error(:not_loaded)
|
||||
end
|
||||
|
||||
See the Erlang documentation for more information:
|
||||
https://www.erlang.org/doc/man/erl_nif
|
||||
|
||||
### `@on_definition`
|
||||
|
||||
A hook that will be invoked when each function or macro in the current
|
||||
@@ -611,6 +644,7 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec reserved_attributes() :: map
|
||||
def reserved_attributes() do
|
||||
%{
|
||||
after_compile: %{
|
||||
@@ -1102,7 +1136,7 @@ defmodule Module do
|
||||
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
|
||||
public functions and macros respectively in compiled modules.
|
||||
|
||||
Note that `defines?` returns false for functions and macros that have
|
||||
Note that `defines?` returns `false` for functions and macros that have
|
||||
been defined but then marked as overridable and no other implementation
|
||||
has been provided. You can check the overridable status by calling
|
||||
`overridable?/2`.
|
||||
@@ -1337,8 +1371,8 @@ defmodule Module do
|
||||
@doc """
|
||||
Deletes a definition from a module.
|
||||
|
||||
It returns true if the definition exists and it was removed,
|
||||
otherwise it returns false.
|
||||
It returns `true` if the definition exists and it was removed,
|
||||
otherwise it returns `false`.
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec delete_definition(module, definition) :: boolean()
|
||||
@@ -1444,7 +1478,7 @@ defmodule Module do
|
||||
Returns `true` if `tuple` in `module` was marked as overridable
|
||||
at some point.
|
||||
|
||||
Note `overridable?/2` returns true even if the definition was
|
||||
Note `overridable?/2` returns `true` even if the definition was
|
||||
already overridden. You can use `defines?/2` to see if a definition
|
||||
exists or one is pending.
|
||||
"""
|
||||
@@ -2099,8 +2133,7 @@ defmodule Module do
|
||||
end
|
||||
|
||||
defp attribute_stack(module, line) do
|
||||
file = String.to_charlist(Path.relative_to_cwd(:elixir_module.file(module)))
|
||||
[{module, :__MODULE__, 0, file: file, line: line}]
|
||||
struct!(Macro.Env, module: module, file: :elixir_module.file(module), line: line)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
@@ -2190,6 +2223,16 @@ defmodule Module do
|
||||
end
|
||||
end
|
||||
|
||||
defp preprocess_attribute(:nifs, value) do
|
||||
unless function_arity_list?(value) do
|
||||
raise ArgumentError,
|
||||
"@nifs is a built-in module attribute for specifying a list " <>
|
||||
"of functions and their arities that are NIFs, got: #{inspect(value)}"
|
||||
end
|
||||
|
||||
value
|
||||
end
|
||||
|
||||
defp preprocess_attribute(:dialyzer, value) do
|
||||
# From https://github.com/erlang/otp/blob/master/lib/stdlib/src/erl_lint.erl
|
||||
:lists.foreach(
|
||||
@@ -2208,17 +2251,22 @@ defmodule Module do
|
||||
value
|
||||
end
|
||||
|
||||
defp valid_dialyzer_attribute?({key, fun_arities}) when is_atom(key) do
|
||||
(key == :nowarn_function or valid_dialyzer_attribute?(key)) and
|
||||
defp function_arity_list?(fun_arities) do
|
||||
is_list(fun_arities) and
|
||||
:lists.all(
|
||||
fn
|
||||
{fun, arity} when is_atom(fun) and is_integer(arity) -> true
|
||||
_ -> false
|
||||
end,
|
||||
List.wrap(fun_arities)
|
||||
fun_arities
|
||||
)
|
||||
end
|
||||
|
||||
defp valid_dialyzer_attribute?({key, fun_arities}) when is_atom(key) do
|
||||
(key == :nowarn_function or valid_dialyzer_attribute?(key)) and
|
||||
function_arity_list?(List.wrap(fun_arities))
|
||||
end
|
||||
|
||||
defp valid_dialyzer_attribute?(attr) do
|
||||
:lists.member(
|
||||
attr,
|
||||
@@ -2226,7 +2274,9 @@ defmodule Module do
|
||||
[:no_match, :no_opaque, :no_fail_call, :no_contracts] ++
|
||||
[:no_behaviours, :no_undefined_callbacks, :unmatched_returns] ++
|
||||
[:error_handling, :race_conditions, :no_missing_calls] ++
|
||||
[:specdiffs, :overspecs, :underspecs, :unknown, :no_underspecs]
|
||||
[:specdiffs, :overspecs, :underspecs, :unknown, :no_underspecs] ++
|
||||
[:extra_return, :no_extra_return, :no_missing_return] ++
|
||||
[:missing_return, :no_unknown]
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ defmodule Module.LocalsTracker do
|
||||
"""
|
||||
def add_local({_set, bag}, from, to, meta, macro_dispatch?)
|
||||
when is_tuple(from) and is_tuple(to) and is_boolean(macro_dispatch?) do
|
||||
put_edge(bag, {:local, from}, {to, get_line(meta), macro_dispatch?})
|
||||
put_edge(bag, {:local, from}, {to, get_position(meta), macro_dispatch?})
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -102,12 +102,21 @@ defmodule Module.LocalsTracker do
|
||||
@doc """
|
||||
Collect undefined functions based on local calls and existing definitions.
|
||||
"""
|
||||
def collect_undefined_locals({set, bag}, all_defined) do
|
||||
def collect_undefined_locals({set, bag}, all_defined, file) do
|
||||
undefined =
|
||||
for {pair, _, meta, _} <- all_defined,
|
||||
{local, line, macro_dispatch?} <- out_neighbours(bag, {:local, pair}),
|
||||
error = undefined_local_error(set, local, macro_dispatch?),
|
||||
do: {pair, build_meta(line, meta), local, error}
|
||||
{{local_name, _} = local, position, macro_dispatch?} <-
|
||||
out_neighbours(bag, {:local, pair}),
|
||||
error = undefined_local_error(set, local, macro_dispatch?) do
|
||||
file =
|
||||
case Keyword.get(meta, :file) do
|
||||
{keep_file, _keep_line} -> keep_file
|
||||
nil -> file
|
||||
end
|
||||
|
||||
meta = build_meta(position, local_name)
|
||||
{pair, meta, file, local, error}
|
||||
end
|
||||
|
||||
:lists.usort(undefined)
|
||||
end
|
||||
@@ -212,16 +221,12 @@ defmodule Module.LocalsTracker do
|
||||
end
|
||||
|
||||
defp get_line(meta), do: Keyword.get(meta, :line)
|
||||
defp get_position(meta), do: {get_line(meta), meta[:column]}
|
||||
|
||||
defp build_meta(nil, _meta), do: []
|
||||
|
||||
# We need to transform any file annotation in the function
|
||||
# definition into a keep annotation that is used by the
|
||||
# error handling system in order to respect line/file.
|
||||
defp build_meta(line, meta) do
|
||||
case Keyword.get(meta, :file) do
|
||||
{file, _} -> [keep: {file, line}]
|
||||
_ -> [line: line]
|
||||
defp build_meta(position, function_name) do
|
||||
case position do
|
||||
{line, nil} -> [line: line]
|
||||
{line, col} -> :elixir_env.calculate_span([line: line, column: col], function_name)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -155,8 +155,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
case :erlang.get(:elixir_code_diagnostics) do
|
||||
:undefined -> :ok
|
||||
{tail, true} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, true})
|
||||
{tail, false} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, false})
|
||||
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
|
||||
end
|
||||
|
||||
diagnostics
|
||||
@@ -168,8 +167,9 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
defp collect_results(count, diagnostics) do
|
||||
receive do
|
||||
{:diagnostic, diagnostic} ->
|
||||
diagnostic = format_diagnostic_file(diagnostic)
|
||||
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
|
||||
diagnostic = %{diagnostic | file: file && Path.absname(file)}
|
||||
collect_results(count, [diagnostic | diagnostics])
|
||||
|
||||
{__MODULE__, _module, new_diagnostics} ->
|
||||
@@ -288,11 +288,6 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_diagnostic_file(%{file: file} = diagnostic) do
|
||||
%{diagnostic | file: file && Path.absname(file)}
|
||||
end
|
||||
|
||||
## Warning helpers
|
||||
|
||||
defp group_warnings(warnings) do
|
||||
@@ -309,79 +304,88 @@ defmodule Module.ParallelChecker do
|
||||
Enum.flat_map(warnings, fn {module, warning, locations} ->
|
||||
message = module.format_warning(warning)
|
||||
diagnostics = Enum.map(locations, &to_diagnostic(message, &1))
|
||||
log? and :elixir_errors.print_warning([message, ?\n, format_stacktraces(diagnostics)])
|
||||
log? and print_warning(message, diagnostics)
|
||||
diagnostics
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_stacktraces([diagnostic]) do
|
||||
format_diagnostic_stacktrace(diagnostic)
|
||||
defp print_warning(message, [diagnostic]) do
|
||||
:elixir_errors.print_warning(message, diagnostic)
|
||||
end
|
||||
|
||||
defp format_stacktraces(diagnostics) do
|
||||
[
|
||||
"Invalid call found at #{length(diagnostics)} locations:\n",
|
||||
Enum.map(diagnostics, &format_diagnostic_stacktrace/1)
|
||||
]
|
||||
defp print_warning(message, grouped_warnings) do
|
||||
:elixir_errors.print_warning_group(message, grouped_warnings)
|
||||
end
|
||||
|
||||
defp format_diagnostic_stacktrace(%{stacktrace: [stacktrace]}) do
|
||||
[" ", Exception.format_stacktrace_entry(stacktrace), ?\n]
|
||||
end
|
||||
|
||||
defp to_diagnostic(message, {file, line, mfa}) do
|
||||
defp to_diagnostic(message, {file, position, mfa}) when is_list(position) do
|
||||
%{
|
||||
severity: :warning,
|
||||
source: file,
|
||||
file: file,
|
||||
position: line,
|
||||
position: position_to_tuple(position),
|
||||
message: IO.iodata_to_binary(message),
|
||||
stacktrace: [to_stacktrace(file, line, mfa)]
|
||||
stacktrace: [to_stacktrace(file, position, mfa)],
|
||||
span: nil
|
||||
}
|
||||
end
|
||||
|
||||
defp to_stacktrace(file, line, {module, fun, arity}),
|
||||
do: {module, fun, arity, location(file, line)}
|
||||
defp position_to_tuple(position) do
|
||||
case position[:column] do
|
||||
nil -> position[:line] || 0
|
||||
col -> {position[:line], col}
|
||||
end
|
||||
end
|
||||
|
||||
defp to_stacktrace(file, line, nil),
|
||||
do: {:elixir_compiler, :__FILE__, 1, location(file, line)}
|
||||
defp to_stacktrace(file, pos, {module, fun, arity}),
|
||||
do: {module, fun, arity, location(file, pos)}
|
||||
|
||||
defp to_stacktrace(file, line, module),
|
||||
do: {module, :__MODULE__, 0, location(file, line)}
|
||||
defp to_stacktrace(file, pos, nil),
|
||||
do: {:elixir_compiler, :__FILE__, 1, location(file, pos)}
|
||||
|
||||
defp location(file, line) do
|
||||
[file: String.to_charlist(Path.relative_to_cwd(file)), line: line]
|
||||
defp to_stacktrace(file, pos, module),
|
||||
do: {module, :__MODULE__, 0, location(file, pos)}
|
||||
|
||||
defp location(file, position) do
|
||||
[{:file, String.to_charlist(Path.relative_to_cwd(file))} | position]
|
||||
end
|
||||
|
||||
## Cache
|
||||
|
||||
defp cache_module({server, ets}, module) do
|
||||
if lock(server, module) do
|
||||
cache_from_chunk(ets, module) || cache_from_info(ets, module)
|
||||
object_code = :code.get_object_code(module)
|
||||
|
||||
# The chunk has more information, so that's our preference
|
||||
with {^module, binary, _filename} <- object_code,
|
||||
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
|
||||
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
|
||||
cache_chunk(ets, module, contents.exports)
|
||||
else
|
||||
_ ->
|
||||
# Otherwise, if the module is loaded, use its info
|
||||
case :erlang.module_loaded(module) do
|
||||
true ->
|
||||
{mode, exports} = info_exports(module)
|
||||
deprecated = info_deprecated(module)
|
||||
cache_info(ets, module, exports, deprecated, mode)
|
||||
|
||||
false ->
|
||||
# Or load exports from chunk
|
||||
with {^module, binary, _filename} <- object_code,
|
||||
{:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do
|
||||
exports = Map.new(Enum.map(exports, &{&1, :def}))
|
||||
cache_info(ets, module, exports, %{}, :erlang)
|
||||
else
|
||||
_ ->
|
||||
:ets.insert(ets, {{:cached, module}, false})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unlock(server, module)
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_from_chunk(ets, module) do
|
||||
with {^module, binary, _filename} <- :code.get_object_code(module),
|
||||
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
|
||||
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
|
||||
cache_chunk(ets, module, contents.exports)
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_from_info(ets, module) do
|
||||
if Code.ensure_loaded?(module) do
|
||||
{mode, exports} = info_exports(module)
|
||||
deprecated = info_deprecated(module)
|
||||
cache_info(ets, module, exports, deprecated, mode)
|
||||
else
|
||||
:ets.insert(ets, {{:cached, module}, false})
|
||||
end
|
||||
end
|
||||
|
||||
defp info_exports(module) do
|
||||
map =
|
||||
Map.new(
|
||||
|
||||
@@ -137,7 +137,7 @@ defmodule Module.Types do
|
||||
# Collect relevant information from context and traces to report error
|
||||
def error_to_warning(:unable_apply, {mfa, args, expected, signature, stack}, context) do
|
||||
{fun, arity} = context.function
|
||||
location = {context.file, get_line(stack), {context.module, fun, arity}}
|
||||
location = {context.file, get_position(stack), {context.module, fun, arity}}
|
||||
|
||||
traces = type_traces(stack, context)
|
||||
{[signature | args], traces} = lift_all_types([signature | args], traces, context)
|
||||
@@ -147,7 +147,7 @@ defmodule Module.Types do
|
||||
|
||||
def error_to_warning(:unable_unify, {left, right, stack}, context) do
|
||||
{fun, arity} = context.function
|
||||
location = {context.file, get_line(stack), {context.module, fun, arity}}
|
||||
location = {context.file, get_position(stack), {context.module, fun, arity}}
|
||||
|
||||
traces = type_traces(stack, context)
|
||||
{[left, right], traces} = lift_all_types([left, right], traces, context)
|
||||
@@ -155,7 +155,9 @@ defmodule Module.Types do
|
||||
{Module.Types, error, location}
|
||||
end
|
||||
|
||||
defp get_line(stack), do: stack.last_expr |> get_meta() |> Keyword.get(:line, 0)
|
||||
defp get_position(stack) do
|
||||
get_meta(stack.last_expr)
|
||||
end
|
||||
|
||||
# Collect relevant traces from context.traces using stack.unify_stack
|
||||
defp type_traces(stack, context) do
|
||||
@@ -349,8 +351,8 @@ defmodule Module.Types do
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_location({file, line, _mfa}) do
|
||||
format_location({file, line})
|
||||
defp format_location({file, position, _mfa}) do
|
||||
format_location({file, position[:line]})
|
||||
end
|
||||
|
||||
defp format_location({file, line}) do
|
||||
@@ -411,14 +413,14 @@ defmodule Module.Types do
|
||||
|
||||
defp format_message_hint(:inferred_dot) do
|
||||
"""
|
||||
HINT: "var.field" (without parentheses) implies "var" is a map() while \
|
||||
#{hint()} "var.field" (without parentheses) implies "var" is a map() while \
|
||||
"var.fun()" (with parentheses) implies "var" is an atom()
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_message_hint(:inferred_bitstring_spec) do
|
||||
"""
|
||||
HINT: all expressions given to binaries are assumed to be of type \
|
||||
#{hint()} all expressions given to binaries are assumed to be of type \
|
||||
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
|
||||
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
|
||||
to change the default behaviour.
|
||||
@@ -427,11 +429,13 @@ defmodule Module.Types do
|
||||
|
||||
defp format_message_hint({:sized_and_unsize_tuples, {size, var}}) do
|
||||
"""
|
||||
HINT: use pattern matching or "is_tuple(#{Macro.to_string(var)}) and \
|
||||
#{hint()} use pattern matching or "is_tuple(#{Macro.to_string(var)}) and \
|
||||
tuple_size(#{Macro.to_string(var)}) == #{size}" to guard a sized tuple.
|
||||
"""
|
||||
end
|
||||
|
||||
defp hint, do: :elixir_errors.prefix(:hint)
|
||||
|
||||
defp format_type_hint(type, types, expr, hints) do
|
||||
case format_type_hint(type, types, expr) do
|
||||
{message, hint} -> {message, [hint | hints]}
|
||||
|
||||
@@ -41,7 +41,8 @@ defmodule Module.Types.Behaviour do
|
||||
end
|
||||
|
||||
defp warn(context, warning, meta \\ []) do
|
||||
location = {meta[:file] || context.file, meta[:line] || context.line, context.module}
|
||||
meta = Keyword.put_new(meta, :line, context.line)
|
||||
location = {meta[:file] || context.file, meta, context.module}
|
||||
|
||||
update_in(context.warnings, &[{__MODULE__, warning, location} | &1])
|
||||
end
|
||||
@@ -68,7 +69,7 @@ defmodule Module.Types.Behaviour do
|
||||
|
||||
context =
|
||||
case context.callbacks do
|
||||
%{^callback => {_kind, conflict, _optional?}} ->
|
||||
%{^callback => [{_kind, conflict, _optional?} | _]} ->
|
||||
warn(
|
||||
context,
|
||||
{:duplicate_behaviour, context.module, behaviour, conflict, kind, callback}
|
||||
@@ -78,11 +79,15 @@ defmodule Module.Types.Behaviour do
|
||||
context
|
||||
end
|
||||
|
||||
put_in(context.callbacks[callback], {kind, behaviour, original in optional_callbacks})
|
||||
new_callback = {kind, behaviour, original in optional_callbacks}
|
||||
callbacks = context.callbacks[callback] || []
|
||||
|
||||
put_in(context.callbacks[callback], [new_callback | callbacks])
|
||||
end
|
||||
|
||||
defp check_callbacks(context, all_definitions) do
|
||||
for {callback, {kind, behaviour, optional?}} <- context.callbacks,
|
||||
for {callback, callbacks} <- context.callbacks,
|
||||
{kind, behaviour, optional?} <- callbacks,
|
||||
reduce: context do
|
||||
context ->
|
||||
case :lists.keyfind(callback, 1, all_definitions) do
|
||||
@@ -132,7 +137,7 @@ defmodule Module.Types.Behaviour do
|
||||
|
||||
{:error, message} ->
|
||||
warning = message |> Tuple.insert_at(1, kind) |> Tuple.insert_at(1, fa)
|
||||
context = warn(context, warning, %{line: line, file: file})
|
||||
context = warn(context, warning, line: line, file: file)
|
||||
{context, impl_contexts}
|
||||
end
|
||||
end)
|
||||
@@ -185,10 +190,10 @@ defmodule Module.Types.Behaviour do
|
||||
end
|
||||
|
||||
defp behaviour_callbacks_for_impls([fa | tail], behaviour, callbacks) do
|
||||
case callbacks[fa] do
|
||||
{_, ^behaviour, _} ->
|
||||
[{fa, behaviour} | behaviour_callbacks_for_impls(tail, behaviour, callbacks)]
|
||||
|
||||
with list when is_list(list) <- callbacks[fa],
|
||||
true <- Enum.any?(list, &match?({_, ^behaviour, _}, &1)) do
|
||||
[{fa, behaviour} | behaviour_callbacks_for_impls(tail, behaviour, callbacks)]
|
||||
else
|
||||
_ ->
|
||||
behaviour_callbacks_for_impls(tail, behaviour, callbacks)
|
||||
end
|
||||
@@ -200,8 +205,12 @@ defmodule Module.Types.Behaviour do
|
||||
|
||||
defp callbacks_for_impls([fa | tail], callbacks) do
|
||||
case callbacks[fa] do
|
||||
{_, behaviour, _} -> [{fa, behaviour} | callbacks_for_impls(tail, callbacks)]
|
||||
nil -> callbacks_for_impls(tail, callbacks)
|
||||
list when is_list(list) ->
|
||||
Enum.map(list, fn {_, behaviour, _} -> {fa, behaviour} end) ++
|
||||
callbacks_for_impls(tail, callbacks)
|
||||
|
||||
nil ->
|
||||
callbacks_for_impls(tail, callbacks)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -213,11 +222,14 @@ defmodule Module.Types.Behaviour do
|
||||
defp warn_missing_impls(context, impl_contexts, defs) do
|
||||
for {pair, kind, meta, _clauses} <- defs, kind in [:def, :defmacro], reduce: context do
|
||||
context ->
|
||||
with {:ok, {_, behaviour, _}} <- Map.fetch(context.callbacks, pair),
|
||||
true <- missing_impl_in_context?(meta, behaviour, impl_contexts) do
|
||||
warn(context, {:missing_impl, pair, kind, behaviour}, %{
|
||||
with {:ok, callbacks} <- Map.fetch(context.callbacks, pair),
|
||||
{_, behaviour, _} <-
|
||||
Enum.find(callbacks, fn {_, behaviour, _} ->
|
||||
missing_impl_in_context?(meta, behaviour, impl_contexts)
|
||||
end) do
|
||||
warn(context, {:missing_impl, pair, kind, behaviour},
|
||||
line: :elixir_utils.get_line(meta)
|
||||
})
|
||||
)
|
||||
else
|
||||
_ -> context
|
||||
end
|
||||
@@ -247,7 +259,7 @@ defmodule Module.Types.Behaviour do
|
||||
|
||||
defp known_callbacks(callbacks) do
|
||||
formatted_callbacks =
|
||||
for {{name, arity}, {kind, module, _}} <- callbacks do
|
||||
for {{name, arity}, list} <- callbacks, {kind, module, _} <- list do
|
||||
"\n * " <> Exception.format_mfa(module, name, arity) <> " (#{format_definition(kind)})"
|
||||
end
|
||||
|
||||
@@ -289,7 +301,7 @@ defmodule Module.Types.Behaviour do
|
||||
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
|
||||
when conflict == behaviour do
|
||||
[
|
||||
"the behavior ",
|
||||
"the behaviour ",
|
||||
inspect(behaviour),
|
||||
" has been declared twice (conflict in ",
|
||||
format_definition(kind, callback),
|
||||
@@ -301,9 +313,9 @@ defmodule Module.Types.Behaviour do
|
||||
|
||||
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
|
||||
[
|
||||
"conflicting behaviours found. ",
|
||||
"conflicting behaviours found. Callback ",
|
||||
format_definition(kind, callback),
|
||||
" is required by ",
|
||||
" is defined by both ",
|
||||
inspect(conflict),
|
||||
" and ",
|
||||
inspect(behaviour),
|
||||
|
||||
@@ -391,10 +391,10 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
# expr.fun(arg)
|
||||
def of_expr({{:., meta1, [expr1, fun]}, _meta2, args} = expr2, _expected, stack, context) do
|
||||
def of_expr({{:., _meta1, [expr1, fun]}, meta2, args} = expr2, _expected, stack, context) do
|
||||
# TODO: Use expected type to infer intersection return type
|
||||
|
||||
context = Of.remote(expr1, fun, length(args), meta1, context)
|
||||
context = Of.remote(expr1, fun, length(args), meta2, context)
|
||||
stack = push_expr_stack(expr2, stack)
|
||||
|
||||
with {:ok, _expr_type, context} <- of_expr(expr1, :dynamic, stack, context),
|
||||
@@ -407,7 +407,7 @@ defmodule Module.Types.Expr do
|
||||
|
||||
# &Foo.bar/1
|
||||
def of_expr(
|
||||
{:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]},
|
||||
{:&, _, [{:/, _, [{{:., _, [module, fun]}, meta, []}, arity]}]},
|
||||
_expected,
|
||||
_stack,
|
||||
context
|
||||
|
||||
@@ -284,11 +284,11 @@ defmodule Module.Types.Of do
|
||||
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, context) do
|
||||
case :otp_internal.obsolete(module, fun, arity) do
|
||||
{:deprecated, string} when is_list(string) ->
|
||||
reason = string |> List.to_string() |> String.capitalize()
|
||||
reason = string |> List.to_string() |> :string.titlecase()
|
||||
warn(meta, context, {:deprecated, module, fun, arity, reason})
|
||||
|
||||
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
|
||||
reason = string |> List.to_string() |> String.capitalize()
|
||||
reason = string |> List.to_string() |> :string.titlecase()
|
||||
reason = "It will be removed in #{removal}. #{reason}"
|
||||
warn(meta, context, {:deprecated, module, fun, arity, reason})
|
||||
|
||||
@@ -323,7 +323,7 @@ defmodule Module.Types.Of do
|
||||
|
||||
defp warn(meta, context, warning) do
|
||||
{fun, arity} = context.function
|
||||
location = {context.file, meta[:line] || 0, {context.module, fun, arity}}
|
||||
location = {context.file, meta, {context.module, fun, arity}}
|
||||
%{context | warnings: [{__MODULE__, warning, location} | context.warnings]}
|
||||
end
|
||||
|
||||
|
||||
@@ -531,7 +531,7 @@ defmodule Module.Types.Unify do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if it is a singleton type.
|
||||
Returns `true` if it is a singleton type.
|
||||
|
||||
Only atoms are singleton types. Unbound vars are not
|
||||
considered singleton types.
|
||||
|
||||
@@ -38,6 +38,12 @@ defmodule OptionParser do
|
||||
]
|
||||
|
||||
defmodule ParseError do
|
||||
@moduledoc """
|
||||
An exception raised when parsing option fails.
|
||||
|
||||
For example, see `OptionParser.parse!/2`.
|
||||
"""
|
||||
|
||||
defexception [:message]
|
||||
end
|
||||
|
||||
@@ -128,7 +134,7 @@ defmodule OptionParser do
|
||||
Switches can be specified with modifiers, which change how
|
||||
they behave. The following modifiers are supported:
|
||||
|
||||
* `:keep` - keeps duplicated elements instead of overriding them;
|
||||
* `:keep` - keeps duplicate elements instead of overriding them;
|
||||
works with all types except `:count`. Specifying `switch_name: :keep`
|
||||
assumes the type of `:switch_name` will be `:string`.
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ defmodule PartitionSupervisor do
|
||||
`name` is the name of the `PartitionSupervisor` and key is used
|
||||
for routing.
|
||||
|
||||
This module was introduced in Elixir v1.14.0.
|
||||
|
||||
## Simple Example
|
||||
|
||||
Let's start with an example which is not useful per se, but shows how the
|
||||
@@ -29,7 +31,7 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
|
||||
def init(args) do
|
||||
IO.inspect [__MODULE__, " got args ", args, " in ", self()]
|
||||
IO.inspect([__MODULE__, " got args ", args, " in ", self()])
|
||||
{:ok, _initial_state = []}
|
||||
end
|
||||
|
||||
@@ -39,7 +41,7 @@ defmodule PartitionSupervisor do
|
||||
|
||||
def handle_call({:collect, msg}, _from, state) do
|
||||
new_state = [msg | state]
|
||||
IO.inspect ["current messages:", new_state, " in process", self()]
|
||||
IO.inspect(["current messages:", new_state, " in process", self()])
|
||||
{:reply, :ok, new_state}
|
||||
end
|
||||
end
|
||||
@@ -134,6 +136,8 @@ defmodule PartitionSupervisor do
|
||||
you can use `GenServer.whereis({:via, PartitionSupervisor, {name, key}})`.
|
||||
"""
|
||||
|
||||
@moduledoc since: "1.14.0"
|
||||
|
||||
@behaviour Supervisor
|
||||
|
||||
@registry PartitionSupervisor.Registry
|
||||
@@ -141,6 +145,7 @@ defmodule PartitionSupervisor do
|
||||
@typedoc """
|
||||
The name of the `PartitionSupervisor`.
|
||||
"""
|
||||
@typedoc since: "1.14.0"
|
||||
@type name :: atom() | {:via, module(), term()}
|
||||
|
||||
@doc false
|
||||
|
||||
+197
-101
@@ -44,17 +44,17 @@ defmodule Path do
|
||||
"""
|
||||
@spec absname(t) :: binary
|
||||
def absname(path) do
|
||||
absname(path, File.cwd!())
|
||||
absname(path, &File.cwd!/0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a path from `relative_to` to `path`.
|
||||
|
||||
If `path` is already an absolute path, `relative_to` is ignored. See also
|
||||
`relative_to/2`.
|
||||
`relative_to/3`. `relative_to` is either a path or an anonymous function,
|
||||
which is invoked only when necessary, that returns a path.
|
||||
|
||||
Unlike `expand/2`, no attempt is made to
|
||||
resolve `..`, `.` or `~`.
|
||||
Unlike `expand/2`, no attempt is made to resolve `..`, `.` or `~`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -65,19 +65,33 @@ defmodule Path do
|
||||
"bar/../x"
|
||||
|
||||
"""
|
||||
@spec absname(t, t) :: binary
|
||||
@spec absname(t, t | (-> t)) :: binary
|
||||
def absname(path, relative_to) do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
case type(path) do
|
||||
:relative ->
|
||||
relative_to =
|
||||
if is_function(relative_to, 0) do
|
||||
relative_to.()
|
||||
else
|
||||
relative_to
|
||||
end
|
||||
|
||||
absname_join([relative_to, path])
|
||||
|
||||
:absolute ->
|
||||
absname_join([path])
|
||||
|
||||
:volumerelative ->
|
||||
relative_to = IO.chardata_to_string(relative_to)
|
||||
relative_to =
|
||||
if is_function(relative_to, 0) do
|
||||
relative_to.()
|
||||
else
|
||||
relative_to
|
||||
end
|
||||
|> IO.chardata_to_string()
|
||||
|
||||
absname_vr(split(path), split(relative_to), relative_to)
|
||||
end
|
||||
end
|
||||
@@ -163,7 +177,7 @@ defmodule Path do
|
||||
"""
|
||||
@spec expand(t) :: binary
|
||||
def expand(path) do
|
||||
expand_dot(absname(expand_home(path), File.cwd!()))
|
||||
expand_dot(absname(expand_home(path), &File.cwd!/0))
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -192,7 +206,7 @@ defmodule Path do
|
||||
"""
|
||||
@spec expand(t, t) :: binary
|
||||
def expand(path, relative_to) do
|
||||
expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), File.cwd!()))
|
||||
expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), &File.cwd!/0))
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -226,6 +240,8 @@ defmodule Path do
|
||||
@doc """
|
||||
Forces the path to be a relative path.
|
||||
|
||||
If an absolute path is given, it is stripped from its root component.
|
||||
|
||||
## Examples
|
||||
|
||||
### Unix-like operating systems
|
||||
@@ -242,6 +258,12 @@ defmodule Path do
|
||||
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
|
||||
|
||||
"""
|
||||
# Note this function does not expand paths because the behaviour
|
||||
# is ambiguous. If we expand it before converting to relative, then
|
||||
# "/usr/../../foo" means "/foo". If we expand it after, it means "../foo".
|
||||
# We could expand only relative paths but it is best to say it never
|
||||
# expands and then provide a `Path.expand_relative` function (or an
|
||||
# option) if desired.
|
||||
@spec relative(t) :: binary
|
||||
def relative(name) do
|
||||
relative(name, major_os_type())
|
||||
@@ -297,52 +319,141 @@ defmodule Path do
|
||||
defp win32_pathtype(relative), do: {:relative, relative}
|
||||
|
||||
@doc """
|
||||
Returns the direct relative path from `path` in relation to `from`.
|
||||
Returns the direct relative path from `path` in relation to `cwd`.
|
||||
|
||||
In other words, this function tries to strip the `from` prefix from `path`.
|
||||
In other words, this function attempts to return a path such that
|
||||
`Path.expand(result, cwd)` points to `path`. This function aims
|
||||
to return a relative path whenever possible, but that's not guaranteed:
|
||||
|
||||
This function does not query the file system, so it assumes
|
||||
no symlinks between the paths.
|
||||
* If both paths are relative, a relative path is always returned
|
||||
|
||||
In case a direct relative path cannot be found, it returns
|
||||
the original path.
|
||||
* If both paths are absolute, a relative path may be returned if
|
||||
they share a common prefix. You can pass the `:force` option to
|
||||
force this function to traverse up, but even then a relative
|
||||
path is not guaranteed (for example, if the absolute paths
|
||||
belong to different drives on Windows)
|
||||
|
||||
* If a mixture of paths are given, the result will always match
|
||||
the given `path` (the first argument)
|
||||
|
||||
This function expands `.` and `..` entries without traversing the
|
||||
file system, so it assumes no symlinks between the paths. See
|
||||
`safe_relative_to/2` for a safer alternative.
|
||||
|
||||
## Options
|
||||
|
||||
* `:force` - (boolean since v1.16.0) if `true` forces a relative
|
||||
path to be returned by traversing the path up. Except if the paths
|
||||
are in different volumes on Windows. Defaults to `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/usr/local")
|
||||
"foo"
|
||||
### With relative `cwd`
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/")
|
||||
"usr/local/foo"
|
||||
If both paths are relative, a minimum path is computed:
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/etc")
|
||||
"/usr/local/foo"
|
||||
Path.relative_to("tmp/foo/bar", "tmp") #=> "foo/bar"
|
||||
Path.relative_to("tmp/foo/bar", "tmp/foo") #=> "bar"
|
||||
Path.relative_to("tmp/foo/bar", "tmp/bat") #=> "../foo/bar"
|
||||
|
||||
iex> Path.relative_to("/usr/local/foo", "/usr/local/foo")
|
||||
"."
|
||||
If an absolute path is given with relative `cwd`, it is returned as:
|
||||
|
||||
Path.relative_to("/usr/foo/bar", "tmp/bat") #=> "/usr/foo/bar"
|
||||
|
||||
### With absolute `cwd`
|
||||
|
||||
If both paths are absolute, a relative is computed if possible,
|
||||
without traversing up:
|
||||
|
||||
Path.relative_to("/usr/local/foo", "/usr/local") #=> "foo"
|
||||
Path.relative_to("/usr/local/foo", "/") #=> "usr/local/foo"
|
||||
Path.relative_to("/usr/local/foo", "/etc") #=> "/usr/local/foo"
|
||||
Path.relative_to("/usr/local/foo", "/usr/local/foo") #=> "."
|
||||
Path.relative_to("/usr/local/../foo", "/usr/foo") #=> "."
|
||||
Path.relative_to("/usr/local/../foo/bar", "/usr/foo") #=> "bar"
|
||||
|
||||
If `:force` is set to `true` paths are traversed up:
|
||||
|
||||
Path.relative_to("/usr", "/usr/local", force: true) #=> ".."
|
||||
Path.relative_to("/usr/foo", "/usr/local", force: true) #=> "../foo"
|
||||
Path.relative_to("/usr/../foo/bar", "/etc/foo", force: true) #=> "../../foo/bar"
|
||||
|
||||
If a relative path is given, it is assumed to be relative to the
|
||||
given path, so the path is returned with "." and ".." expanded:
|
||||
|
||||
Path.relative_to(".", "/usr/local") #=> "."
|
||||
Path.relative_to("foo", "/usr/local") #=> "foo"
|
||||
Path.relative_to("foo/../bar", "/usr/local") #=> "bar"
|
||||
Path.relative_to("foo/..", "/usr/local") #=> "."
|
||||
Path.relative_to("../foo", "/usr/local") #=> "../foo"
|
||||
|
||||
"""
|
||||
@spec relative_to(t, t) :: binary
|
||||
def relative_to(path, from) do
|
||||
path = IO.chardata_to_string(path)
|
||||
relative_to(split(path), split(from), path)
|
||||
@spec relative_to(t, t, keyword) :: binary
|
||||
def relative_to(path, cwd, opts \\ []) when is_list(opts) do
|
||||
os_type = major_os_type()
|
||||
split_path = split(path)
|
||||
split_cwd = split(cwd)
|
||||
force = Keyword.get(opts, :force, false)
|
||||
|
||||
case {split_absolute?(split_path, os_type), split_absolute?(split_cwd, os_type)} do
|
||||
{true, true} ->
|
||||
split_path = expand_split(split_path)
|
||||
split_cwd = expand_split(split_cwd)
|
||||
|
||||
case force do
|
||||
true -> relative_to_forced(split_path, split_cwd, split_path)
|
||||
false -> relative_to_unforced(split_path, split_cwd, split_path)
|
||||
end
|
||||
|
||||
{false, false} ->
|
||||
split_path = expand_relative(split_path, [], [])
|
||||
split_cwd = expand_relative(split_cwd, [], [])
|
||||
relative_to_forced(split_path, split_cwd, [])
|
||||
|
||||
{_, _} ->
|
||||
join(expand_relative(split_path, [], []))
|
||||
end
|
||||
end
|
||||
|
||||
defp relative_to(path, path, _original) do
|
||||
"."
|
||||
defp relative_to_unforced(path, path, _original), do: "."
|
||||
|
||||
defp relative_to_unforced([h | t1], [h | t2], original),
|
||||
do: relative_to_unforced(t1, t2, original)
|
||||
|
||||
defp relative_to_unforced([_ | _] = l1, [], _original), do: join(l1)
|
||||
defp relative_to_unforced(_, _, original), do: join(original)
|
||||
|
||||
defp relative_to_forced(path, path, _original), do: "."
|
||||
defp relative_to_forced([h | t1], [h | t2], original), do: relative_to_forced(t1, t2, original)
|
||||
|
||||
# this should only happen if we have two paths on different drives on windows
|
||||
defp relative_to_forced(original, _, original), do: join(original)
|
||||
|
||||
defp relative_to_forced(l1, l2, _original) do
|
||||
base = List.duplicate("..", length(l2))
|
||||
join(base ++ l1)
|
||||
end
|
||||
|
||||
defp relative_to([h | t1], [h | t2], original) do
|
||||
relative_to(t1, t2, original)
|
||||
end
|
||||
defp expand_relative([".." | t], [_ | acc], up), do: expand_relative(t, acc, up)
|
||||
defp expand_relative([".." | t], acc, up), do: expand_relative(t, acc, [".." | up])
|
||||
defp expand_relative(["." | t], acc, up), do: expand_relative(t, acc, up)
|
||||
defp expand_relative([h | t], acc, up), do: expand_relative(t, [h | acc], up)
|
||||
defp expand_relative([], [], []), do: ["."]
|
||||
defp expand_relative([], acc, up), do: up ++ :lists.reverse(acc)
|
||||
|
||||
defp relative_to([_ | _] = l1, [], _original) do
|
||||
join(l1)
|
||||
end
|
||||
defp expand_split([head | tail]), do: expand_split(tail, [head])
|
||||
defp expand_split([".." | t], [_, last | acc]), do: expand_split(t, [last | acc])
|
||||
defp expand_split([".." | t], acc), do: expand_split(t, acc)
|
||||
defp expand_split(["." | t], acc), do: expand_split(t, acc)
|
||||
defp expand_split([h | t], acc), do: expand_split(t, [h | acc])
|
||||
defp expand_split([], acc), do: :lists.reverse(acc)
|
||||
|
||||
defp relative_to(_, _, original) do
|
||||
original
|
||||
end
|
||||
defp split_absolute?(split, :win32), do: win32_split_absolute?(split)
|
||||
defp split_absolute?(split, _), do: match?(["/" | _], split)
|
||||
|
||||
defp win32_split_absolute?(["//" | _]), do: true
|
||||
defp win32_split_absolute?([<<_, ":/">> | _]), do: true
|
||||
defp win32_split_absolute?(_), do: false
|
||||
|
||||
@doc """
|
||||
Convenience to get the path relative to the current working
|
||||
@@ -350,11 +461,13 @@ defmodule Path do
|
||||
|
||||
If, for some reason, the current working directory
|
||||
cannot be retrieved, this function returns the given `path`.
|
||||
|
||||
Check `relative_to/3` for the supported options.
|
||||
"""
|
||||
@spec relative_to_cwd(t) :: binary
|
||||
def relative_to_cwd(path) do
|
||||
@spec relative_to_cwd(t, keyword) :: binary
|
||||
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
|
||||
case :file.get_cwd() do
|
||||
{:ok, base} -> relative_to(path, IO.chardata_to_string(base))
|
||||
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
|
||||
_ -> path
|
||||
end
|
||||
end
|
||||
@@ -614,7 +727,7 @@ defmodule Path do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@doc ~S"""
|
||||
Traverses paths according to the given `glob` expression and returns a
|
||||
list of matches.
|
||||
|
||||
@@ -646,9 +759,9 @@ defmodule Path do
|
||||
You may call `Path.expand/1` to normalize the path before invoking
|
||||
this function.
|
||||
|
||||
A character preceded by \ loses its special meaning.
|
||||
Note that \ must be written as \\ in a string literal.
|
||||
For example, "\\?*" will match any filename starting with ?.
|
||||
A character preceded by `\\` loses its special meaning.
|
||||
Note that `\\` must be written as `\\\\` in a string literal.
|
||||
For example, `"\\\\?*"` will match any filename starting with `?.`.
|
||||
|
||||
By default, the patterns `*` and `?` do not match files starting
|
||||
with a dot `.`. See the `:match_dot` option in the "Options" section
|
||||
@@ -718,21 +831,18 @@ defmodule Path do
|
||||
end
|
||||
end
|
||||
|
||||
# expand_dot the given path by expanding "..", "." and "~".
|
||||
defp expand_dot(<<"/", rest::binary>>), do: "/" <> do_expand_dot(rest)
|
||||
# expands dots in an absolute path represented as a string
|
||||
defp expand_dot(path) do
|
||||
[head | tail] = :binary.split(path, "/", [:global])
|
||||
IO.iodata_to_binary(expand_dot(tail, [head <> "/"]))
|
||||
end
|
||||
|
||||
defp expand_dot(<<letter, ":/", rest::binary>>) when letter in ?a..?z,
|
||||
do: <<letter, ":/">> <> do_expand_dot(rest)
|
||||
|
||||
defp expand_dot(path), do: do_expand_dot(path)
|
||||
|
||||
defp do_expand_dot(path), do: do_expand_dot(:binary.split(path, "/", [:global]), [])
|
||||
defp do_expand_dot([".." | t], [_, _ | acc]), do: do_expand_dot(t, acc)
|
||||
defp do_expand_dot([".." | t], []), do: do_expand_dot(t, [])
|
||||
defp do_expand_dot(["." | t], acc), do: do_expand_dot(t, acc)
|
||||
defp do_expand_dot([h | t], acc), do: do_expand_dot(t, ["/", h | acc])
|
||||
defp do_expand_dot([], []), do: ""
|
||||
defp do_expand_dot([], ["/" | acc]), do: IO.iodata_to_binary(:lists.reverse(acc))
|
||||
defp expand_dot([".." | t], [_, _ | acc]), do: expand_dot(t, acc)
|
||||
defp expand_dot([".." | t], acc), do: expand_dot(t, acc)
|
||||
defp expand_dot(["." | t], acc), do: expand_dot(t, acc)
|
||||
defp expand_dot([h | t], acc), do: expand_dot(t, ["/", h | acc])
|
||||
defp expand_dot([], ["/", head | acc]), do: :lists.reverse([head | acc])
|
||||
defp expand_dot([], acc), do: :lists.reverse(acc)
|
||||
|
||||
defp major_os_type do
|
||||
:os.type() |> elem(0)
|
||||
@@ -741,6 +851,18 @@ defmodule Path do
|
||||
@doc """
|
||||
Returns a relative path that is protected from directory-traversal attacks.
|
||||
|
||||
See `safe_relative/2` for a non-deprecated version of this API.
|
||||
"""
|
||||
# TODO: Deprecate me on Elixir v1.19
|
||||
@doc since: "1.14.0", deprecated: "Use safe_relative/2 instead"
|
||||
@spec safe_relative_to(t, t) :: {:ok, binary} | :error
|
||||
def safe_relative_to(path, cwd) do
|
||||
safe_relative(path, cwd)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a relative path that is protected from directory-traversal attacks.
|
||||
|
||||
The given relative path is sanitized by eliminating `..` and `.` components.
|
||||
|
||||
This function checks that, after expanding those components, the path is still "safe".
|
||||
@@ -748,63 +870,37 @@ defmodule Path do
|
||||
|
||||
* The path is not relative, such as `"/foo/bar"`.
|
||||
|
||||
* A `..` component would make it so that the path would travers up above
|
||||
* A `..` component would make it so that the path would traverse up above
|
||||
the root of `relative_to`.
|
||||
|
||||
* A symbolic link in the path points to something above the root of `relative_to`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.safe_relative_to("deps/my_dep/app.beam", "deps")
|
||||
{:ok, "deps/my_dep/app.beam"}
|
||||
|
||||
iex> Path.safe_relative_to("deps/my_dep/./build/../app.beam", "deps")
|
||||
{:ok, "deps/my_dep/app.beam"}
|
||||
|
||||
iex> Path.safe_relative_to("my_dep/../..", "deps")
|
||||
:error
|
||||
|
||||
iex> Path.safe_relative_to("/usr/local", ".")
|
||||
:error
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec safe_relative_to(t, t) :: {:ok, binary} | :error
|
||||
def safe_relative_to(path, relative_to) do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
case :filelib.safe_relative_path(path, relative_to) do
|
||||
:unsafe -> :error
|
||||
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a path relative to the current working directory that is
|
||||
protected from directory-traversal attacks.
|
||||
|
||||
Same as `safe_relative_to/2` with the current working directory as
|
||||
the second argument. If there is an issue retrieving the current working
|
||||
directory, this function raises an error.
|
||||
* A symbolic link in the path points to something above the root of `cwd`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Path.safe_relative("foo")
|
||||
{:ok, "foo"}
|
||||
|
||||
iex> Path.safe_relative("foo/../bar")
|
||||
{:ok, "bar"}
|
||||
iex> Path.safe_relative("deps/my_dep/app.beam")
|
||||
{:ok, "deps/my_dep/app.beam"}
|
||||
|
||||
iex> Path.safe_relative("foo/../..")
|
||||
iex> Path.safe_relative("deps/my_dep/./build/../app.beam", File.cwd!())
|
||||
{:ok, "deps/my_dep/app.beam"}
|
||||
|
||||
iex> Path.safe_relative("my_dep/../..")
|
||||
:error
|
||||
|
||||
iex> Path.safe_relative("/usr/local")
|
||||
iex> Path.safe_relative("/usr/local", File.cwd!())
|
||||
:error
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec safe_relative(t) :: {:ok, binary} | :error
|
||||
def safe_relative(path) do
|
||||
safe_relative_to(path, File.cwd!())
|
||||
@spec safe_relative(t, t) :: {:ok, binary} | :error
|
||||
def safe_relative(path, cwd \\ File.cwd!()) do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
case :filelib.safe_relative_path(path, cwd) do
|
||||
:unsafe -> :error
|
||||
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,27 +79,6 @@ defmodule Port do
|
||||
are for advanced usage within the VM. Also consider using `System.cmd/3`
|
||||
if all you want is to execute a program and retrieve its return value.
|
||||
|
||||
> #### Windows argument splitting and untrusted arguments {: .warning}
|
||||
>
|
||||
> On Unix systems, arguments are passed to a new operating system
|
||||
> process as an array of strings but on Windows it is up to the child
|
||||
> process to parse them and some Windows programs may apply their own
|
||||
> rules, which are inconsistent with the standard C runtime `argv` parsing
|
||||
>
|
||||
> This is particularly troublesome when invoking `.bat` or `.com` files
|
||||
> as these run implicitly through `cmd.exe`, whose argument parsing is
|
||||
> vulnerable to malicious input and can be used to run arbitrary shell
|
||||
> commands.
|
||||
>
|
||||
> Therefore, if you are running on Windows and you execute batch
|
||||
> files or `.com` applications, you must not pass untrusted input as
|
||||
> arguments to the program. You may avoid accidentally executing them
|
||||
> by explicitly passing the extension of the program you want to run,
|
||||
> such as `.exe`, and double check the program is indeed not a batch
|
||||
> file or `.com` application.
|
||||
>
|
||||
> This affects both `spawn` and `spawn_executable`.
|
||||
|
||||
### spawn
|
||||
|
||||
The `:spawn` tuple receives a binary that is going to be executed as a
|
||||
|
||||
@@ -504,7 +504,7 @@ defmodule Process do
|
||||
If the process is already dead when calling `Process.monitor/1`, a
|
||||
`:DOWN` message is delivered immediately.
|
||||
|
||||
See ["The need for monitoring"](https://elixir-lang.org/getting-started/mix-otp/genserver.html#the-need-for-monitoring)
|
||||
See ["The need for monitoring"](genservers.md#the-need-for-monitoring)
|
||||
for an example. See `:erlang.monitor/2` for more information.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -651,7 +651,7 @@ defmodule Process do
|
||||
defdelegate unlink(pid_or_port), to: :erlang
|
||||
|
||||
@doc """
|
||||
Registers the given `pid_or_port` under the given `name`.
|
||||
Registers the given `pid_or_port` under the given `name` on the local node.
|
||||
|
||||
`name` must be an atom and can then be used instead of the
|
||||
PID/port identifier when sending messages with `Kernel.send/2`.
|
||||
|
||||
+22
-20
@@ -684,7 +684,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp load_impl(protocol, for) do
|
||||
Module.concat(protocol, for).__impl__(:target)
|
||||
Module.concat(protocol, for)
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
@@ -826,7 +826,7 @@ defmodule Protocol do
|
||||
quote bind_quoted: [built_in: __built_in__()] do
|
||||
any_impl_for =
|
||||
if @fallback_to_any do
|
||||
quote do: unquote(__MODULE__.Any).__impl__(:target)
|
||||
quote do: unquote(__MODULE__.Any)
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -853,11 +853,9 @@ defmodule Protocol do
|
||||
target = Module.concat(__MODULE__, mod)
|
||||
|
||||
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
|
||||
try do
|
||||
unquote(target).__impl__(:target)
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
unquote(any_impl_for)
|
||||
case Code.ensure_compiled(unquote(target)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
end,
|
||||
@@ -888,14 +886,11 @@ defmodule Protocol do
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
target = Module.concat(__MODULE__, struct)
|
||||
|
||||
try do
|
||||
target.__impl__(:target)
|
||||
rescue
|
||||
UndefinedFunctionError ->
|
||||
unquote(any_impl_for)
|
||||
end
|
||||
target =
|
||||
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
|
||||
# Inline struct implementation for performance
|
||||
@@ -962,10 +957,8 @@ defmodule Protocol do
|
||||
impl =
|
||||
quote unquote: false do
|
||||
@doc false
|
||||
@spec __impl__(:target) :: __MODULE__
|
||||
@spec __impl__(:for) :: unquote(for)
|
||||
@spec __impl__(:protocol) :: unquote(protocol)
|
||||
def __impl__(:target), do: __MODULE__
|
||||
def __impl__(:for), do: unquote(for)
|
||||
def __impl__(:protocol), do: unquote(protocol)
|
||||
end
|
||||
@@ -1025,21 +1018,30 @@ defmodule Protocol do
|
||||
if function_exported?(mod, fun, length(args)) do
|
||||
apply(mod, fun, args)
|
||||
else
|
||||
funs =
|
||||
for {fun, arity} <- protocol.__protocol__(:functions) do
|
||||
args = Macro.generate_arguments(arity, nil)
|
||||
|
||||
quote do
|
||||
def unquote(fun)(unquote_splicing(args)),
|
||||
do: unquote(impl).unquote(fun)(unquote_splicing(args))
|
||||
end
|
||||
end
|
||||
|
||||
quoted =
|
||||
quote do
|
||||
@behaviour unquote(protocol)
|
||||
Module.register_attribute(__MODULE__, :__impl__, persist: true)
|
||||
@__impl__ [protocol: unquote(protocol), for: unquote(for)]
|
||||
|
||||
@doc false
|
||||
@spec __impl__(:target) :: unquote(impl)
|
||||
@spec __impl__(:protocol) :: unquote(protocol)
|
||||
@spec __impl__(:for) :: unquote(for)
|
||||
def __impl__(:target), do: unquote(impl)
|
||||
def __impl__(:protocol), do: unquote(protocol)
|
||||
def __impl__(:for), do: unquote(for)
|
||||
end
|
||||
|
||||
Module.create(Module.concat(protocol, for), quoted, Macro.Env.location(env))
|
||||
Module.create(Module.concat(protocol, for), [quoted | funs], Macro.Env.location(env))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -244,6 +244,7 @@ defmodule Range do
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec size(t) :: non_neg_integer
|
||||
def size(range)
|
||||
def size(first..last//step) when step > 0 and first > last, do: 0
|
||||
def size(first..last//step) when step < 0 and first < last, do: 0
|
||||
@@ -272,6 +273,7 @@ defmodule Range do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec shift(t, integer) :: t
|
||||
def shift(first..last//step, steps_to_shift)
|
||||
when is_integer(steps_to_shift) do
|
||||
new(first + steps_to_shift * step, last + steps_to_shift * step, step)
|
||||
@@ -363,6 +365,7 @@ defmodule Range do
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec split(t, integer) :: {t, t}
|
||||
def split(first..last//step = range, split) when is_integer(split) do
|
||||
if split >= 0 do
|
||||
split(first, last, step, split)
|
||||
@@ -391,8 +394,17 @@ defmodule Range do
|
||||
|
||||
@doc """
|
||||
Converts a range to a list.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Range.to_list(0..5)
|
||||
[0, 1, 2, 3, 4, 5]
|
||||
iex> Range.to_list(-3..0)
|
||||
[-3, -2, -1, 0]
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec to_list(t) :: list(integer)
|
||||
def to_list(first..last//step)
|
||||
when step > 0 and first <= last
|
||||
when step < 0 and first >= last do
|
||||
|
||||
+38
-15
@@ -7,7 +7,7 @@ defmodule Regex do
|
||||
in the [`:re` module documentation](`:re`).
|
||||
|
||||
Regular expressions in Elixir can be created using the sigils
|
||||
`~r` (see `sigil_r/2`) or `~R` (see `sigil_R/2`):
|
||||
`~r` (see `sigil_r/2`):
|
||||
|
||||
# A simple regular expression that matches foo anywhere in the string
|
||||
~r/foo/
|
||||
@@ -34,6 +34,37 @@ defmodule Regex do
|
||||
|
||||
~r/(?<foo>.)(?<bar>.)/.source == ~r/(?<foo>.)(?<bar>.)/.source
|
||||
|
||||
## Escapes
|
||||
|
||||
Escape sequences are split into two categories.
|
||||
|
||||
### Non-printing characters
|
||||
|
||||
* `\a` - Alarm, that is, the BEL character (hex 07)
|
||||
* `\e` - Escape (hex 1B)
|
||||
* `\f` - Form feed (hex 0C)
|
||||
* `\n` - Line feed (hex 0A)
|
||||
* `\r` - Carriage return (hex 0D)
|
||||
* `\t` - Tab (hex 09)
|
||||
* `\xhh` - Character with hex code hh
|
||||
* `\x{hhh..}` - Character with hex code hhh..
|
||||
|
||||
`\u` and `\U` are not supported. Other escape sequences, such as `\ddd`
|
||||
for octals, are supported but discouraged.
|
||||
|
||||
### Generic character types
|
||||
|
||||
* `\d` - Any decimal digit
|
||||
* `\D` - Any character that is not a decimal digit
|
||||
* `\h` - Any horizontal whitespace character
|
||||
* `\H` - Any character that is not a horizontal whitespace character
|
||||
* `\s` - Any whitespace character
|
||||
* `\S` - Any character that is not a whitespace character
|
||||
* `\v` - Any vertical whitespace character
|
||||
* `\V` - Any character that is not a vertical whitespace character
|
||||
* `\w` - Any "word" character
|
||||
* `\W` - Any "non-word" character
|
||||
|
||||
## Modifiers
|
||||
|
||||
The modifiers available when creating a Regex are:
|
||||
@@ -158,6 +189,10 @@ defmodule Regex do
|
||||
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary | [term]}
|
||||
|
||||
defmodule CompileError do
|
||||
@moduledoc """
|
||||
An exception raised when a regular expression could not be compiled.
|
||||
"""
|
||||
|
||||
defexception message: "regex could not be compiled"
|
||||
end
|
||||
|
||||
@@ -512,7 +547,8 @@ defmodule Regex do
|
||||
|
||||
* `:on` - specifies which captures to split the string on, and in what
|
||||
order. Defaults to `:first` which means captures inside the regex do not
|
||||
affect the splitting process.
|
||||
affect the splitting process. Check the moduledoc for `Regex`
|
||||
to see the possible capture values.
|
||||
|
||||
* `:include_captures` - when `true`, includes in the result the matches of
|
||||
the regular expression. The matches are not counted towards the maximum
|
||||
@@ -847,19 +883,6 @@ defmodule Regex do
|
||||
|
||||
# Helpers
|
||||
|
||||
@doc false
|
||||
# Unescape map function used by Macro.unescape_string.
|
||||
def unescape_map(:newline), do: true
|
||||
def unescape_map(?f), do: ?\f
|
||||
def unescape_map(?n), do: ?\n
|
||||
def unescape_map(?r), do: ?\r
|
||||
def unescape_map(?t), do: ?\t
|
||||
def unescape_map(?v), do: ?\v
|
||||
def unescape_map(?a), do: ?\a
|
||||
def unescape_map(_), do: false
|
||||
|
||||
# Private Helpers
|
||||
|
||||
defp translate_options(<<?u, t::binary>>, acc), do: translate_options(t, [:unicode, :ucp | acc])
|
||||
defp translate_options(<<?i, t::binary>>, acc), do: translate_options(t, [:caseless | acc])
|
||||
defp translate_options(<<?x, t::binary>>, acc), do: translate_options(t, [:extended | acc])
|
||||
|
||||
@@ -27,8 +27,8 @@ defmodule Registry do
|
||||
`Registry.start_link/1`, it can be used to register and access named
|
||||
processes using the `{:via, Registry, {registry, key}}` tuple:
|
||||
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest)
|
||||
name = {:via, Registry, {Registry.ViaTest, "agent"}}
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry)
|
||||
name = {:via, Registry, {MyApp.Registry, "agent"}}
|
||||
{:ok, _} = Agent.start_link(fn -> 0 end, name: name)
|
||||
Agent.get(name, & &1)
|
||||
#=> 0
|
||||
@@ -39,22 +39,22 @@ defmodule Registry do
|
||||
In the previous example, we were not interested in associating a value to the
|
||||
process:
|
||||
|
||||
Registry.lookup(Registry.ViaTest, "agent")
|
||||
Registry.lookup(MyApp.Registry, "agent")
|
||||
#=> [{self(), nil}]
|
||||
|
||||
However, in some cases it may be desired to associate a value to the process
|
||||
using the alternate `{:via, Registry, {registry, key, value}}` tuple:
|
||||
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest)
|
||||
name = {:via, Registry, {Registry.ViaTest, "agent", :hello}}
|
||||
{:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry)
|
||||
name = {:via, Registry, {MyApp.Registry, "agent", :hello}}
|
||||
{:ok, agent_pid} = Agent.start_link(fn -> 0 end, name: name)
|
||||
Registry.lookup(Registry.ViaTest, "agent")
|
||||
Registry.lookup(MyApp.Registry, "agent")
|
||||
#=> [{agent_pid, :hello}]
|
||||
|
||||
To this point, we have been starting `Registry` using `start_link/1`.
|
||||
Typically the registry is started as part of a supervision tree though:
|
||||
|
||||
{Registry, keys: :unique, name: Registry.ViaTest}
|
||||
{Registry, keys: :unique, name: MyApp.Registry}
|
||||
|
||||
Only registries with unique keys can be used in `:via`. If the name is
|
||||
already taken, the case-specific `start_link` function (`Agent.start_link/2`
|
||||
@@ -1283,7 +1283,7 @@ defmodule Registry do
|
||||
variables like `:"$1"`, `:"$2"`, and so forth.
|
||||
|
||||
The third part, the body, is a list of shapes of the returned entries. Like guards, you have access to
|
||||
assigned variables like `:"$1"`, which you can combine with hardcoded values to freely shape entries
|
||||
assigned variables like `:"$1"`, which you can combine with hard-coded values to freely shape entries
|
||||
Note that tuples have to be wrapped in an additional tuple. To get a result format like
|
||||
`%{key: key, pid: pid, value: value}`, assuming you bound those variables in order in the match part,
|
||||
you would provide a body like `[%{key: :"$1", pid: :"$2", value: :"$3"}]`. Like guards, you can use
|
||||
|
||||
+136
-11
@@ -18,7 +18,7 @@ defmodule String do
|
||||
"hello world"
|
||||
|
||||
The functions in this module act according to
|
||||
[The Unicode Standard, Version 15.0.0](http://www.unicode.org/versions/Unicode15.0.0/).
|
||||
[The Unicode Standard, Version 15.1.0](http://www.unicode.org/versions/Unicode15.1.0/).
|
||||
|
||||
## Interpolation
|
||||
|
||||
@@ -759,6 +759,7 @@ defmodule String do
|
||||
"fi"
|
||||
|
||||
"""
|
||||
@spec normalize(t, :nfd | :nfc | :nfkd | :nfkc) :: t
|
||||
def normalize(string, form)
|
||||
|
||||
def normalize(string, :nfd) when is_binary(string) do
|
||||
@@ -823,6 +824,7 @@ defmodule String do
|
||||
iex> String.upcase("ıi", :turkic)
|
||||
"Iİ"
|
||||
|
||||
Also see `downcase/2` and `capitalize/2` for other conversions.
|
||||
"""
|
||||
@spec upcase(t, :default | :ascii | :greek | :turkic) :: t
|
||||
def upcase(string, mode \\ :default)
|
||||
@@ -857,6 +859,8 @@ defmodule String do
|
||||
lowercases only the letters A to Z. `:greek` includes the context sensitive
|
||||
mappings found in Greek. `:turkic` properly handles the letter i with the dotless variant.
|
||||
|
||||
Also see `upcase/2` and `capitalize/2` for other conversions.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.downcase("ABCD")
|
||||
@@ -921,19 +925,25 @@ defmodule String do
|
||||
Converts the first character in the given string to
|
||||
uppercase and the remainder to lowercase according to `mode`.
|
||||
|
||||
`mode` may be `:default`, `:ascii`, `:greek` or `:turkic`. The `:default` mode considers
|
||||
all non-conditional transformations outlined in the Unicode standard. `:ascii`
|
||||
capitalizes only the letters A to Z. `:greek` includes the context sensitive
|
||||
mappings found in Greek. `:turkic` properly handles the letter i with the dotless variant.
|
||||
`mode` may be `:default`, `:ascii`, `:greek` or `:turkic`. The `:default` mode
|
||||
considers all non-conditional transformations outlined in the Unicode standard.
|
||||
`:ascii` capitalizes only the letters A to Z. `:greek` includes the context
|
||||
sensitive mappings found in Greek. `:turkic` properly handles the letter `i`
|
||||
with the dotless variant.
|
||||
|
||||
Also see `upcase/2` and `capitalize/2` for other conversions. If you want
|
||||
a variation of this function that does not lowercase the rest of string,
|
||||
see Erlang's `:string.titlecase/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.capitalize("abcd")
|
||||
"Abcd"
|
||||
iex> String.capitalize("ABCD")
|
||||
"Abcd"
|
||||
|
||||
iex> String.capitalize("fin")
|
||||
"Fin"
|
||||
|
||||
iex> String.capitalize("olá")
|
||||
"Olá"
|
||||
|
||||
@@ -946,9 +956,22 @@ defmodule String do
|
||||
<<char>> <> downcase(rest, :ascii)
|
||||
end
|
||||
|
||||
@letter_I <<0x0049::utf8>>
|
||||
@letter_i <<0x0069::utf8>>
|
||||
@letter_I_dot_above <<0x0130::utf8>>
|
||||
|
||||
def capitalize(<<@letter_i, right::binary>>, mode) do
|
||||
if(mode == :turkic, do: @letter_I_dot_above, else: @letter_I) <> downcase(right, mode)
|
||||
end
|
||||
|
||||
def capitalize(string, mode) when is_binary(string) do
|
||||
{char, rest} = String.Unicode.titlecase_once(string, mode)
|
||||
char <> downcase(rest, mode)
|
||||
case :unicode_util.gc(string) do
|
||||
[gc] -> grapheme_to_binary(:string.titlecase([gc]))
|
||||
[gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
|
||||
[gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode)
|
||||
[] -> ""
|
||||
{:error, <<byte, rest::bits>>} -> <<byte>> <> downcase(rest, mode)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -1849,6 +1872,104 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
defguardp replace_invalid_ii_of_iii(i, ii)
|
||||
when Bitwise.bor(Bitwise.bsl(i, 6), ii) in 32..863 or
|
||||
Bitwise.bor(Bitwise.bsl(i, 6), ii) in 896..1023
|
||||
|
||||
defguardp replace_invalid_ii_of_iv(i, ii)
|
||||
when Bitwise.bor(Bitwise.bsl(i, 6), ii) in 16..271
|
||||
|
||||
defguardp replace_invalid_iii_of_iv(i, ii, iii)
|
||||
when Bitwise.bor(Bitwise.bor(Bitwise.bsl(i, 12), Bitwise.bsl(ii, 6)), iii) in 1024..17407
|
||||
|
||||
defguardp replace_invalid_is_next(next) when Bitwise.bsr(next, 6) !== 0b10
|
||||
|
||||
@doc ~S"""
|
||||
Returns a new string created by replacing all invalid bytes with `replacement` (`"�"` by default).
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.replace_invalid("asd" <> <<0xFF::8>>)
|
||||
"asd�"
|
||||
|
||||
iex> String.replace_invalid("nem rán bề bề")
|
||||
"nem rán bề bề"
|
||||
|
||||
iex> String.replace_invalid("nem rán b" <> <<225, 187>> <> " bề")
|
||||
"nem rán b� bề"
|
||||
|
||||
iex> String.replace_invalid("nem rán b" <> <<225, 187>> <> " bề", "ERROR!")
|
||||
"nem rán bERROR! bề"
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
def replace_invalid(bytes, replacement \\ "�")
|
||||
when is_binary(bytes) and is_binary(replacement) do
|
||||
do_replace_invalid(bytes, replacement, <<>>)
|
||||
end
|
||||
|
||||
# Valid ASCII (for better average speed)
|
||||
defp do_replace_invalid(<<ascii::8, next::8, _::bytes>> = rest, rep, acc)
|
||||
when ascii in 0..127 and replace_invalid_is_next(next) do
|
||||
<<_::8, rest::bytes>> = rest
|
||||
do_replace_invalid(rest, rep, acc <> <<ascii::8>>)
|
||||
end
|
||||
|
||||
# Valid UTF-8
|
||||
defp do_replace_invalid(<<grapheme::utf8, rest::bytes>>, rep, acc) do
|
||||
do_replace_invalid(rest, rep, acc <> <<grapheme::utf8>>)
|
||||
end
|
||||
|
||||
# 2/3 truncated sequence
|
||||
defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6>>, rep, acc)
|
||||
when replace_invalid_ii_of_iii(i, ii) do
|
||||
acc <> rep
|
||||
end
|
||||
|
||||
defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6, next::8, _::bytes>> = rest, rep, acc)
|
||||
when replace_invalid_ii_of_iii(i, ii) and replace_invalid_is_next(next) do
|
||||
<<_::16, rest::bytes>> = rest
|
||||
do_replace_invalid(rest, rep, acc <> rep)
|
||||
end
|
||||
|
||||
# 2/4
|
||||
defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6>>, rep, acc)
|
||||
when replace_invalid_ii_of_iv(i, ii) do
|
||||
acc <> rep
|
||||
end
|
||||
|
||||
defp do_replace_invalid(
|
||||
<<0b11110::5, i::3, 0b10::2, ii::6, next::8, _::bytes>> = rest,
|
||||
rep,
|
||||
acc
|
||||
)
|
||||
when replace_invalid_ii_of_iv(i, ii) and replace_invalid_is_next(next) do
|
||||
<<_::16, rest::bytes>> = rest
|
||||
do_replace_invalid(rest, rep, acc <> rep)
|
||||
end
|
||||
|
||||
# 3/4
|
||||
defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6>>, rep, acc)
|
||||
when replace_invalid_iii_of_iv(i, ii, iii) do
|
||||
acc <> rep
|
||||
end
|
||||
|
||||
defp do_replace_invalid(
|
||||
<<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6, next::8, _::bytes>> = rest,
|
||||
rep,
|
||||
acc
|
||||
)
|
||||
when replace_invalid_iii_of_iv(i, ii, iii) and replace_invalid_is_next(next) do
|
||||
<<_::24, rest::bytes>> = rest
|
||||
do_replace_invalid(rest, rep, acc <> rep)
|
||||
end
|
||||
|
||||
# Everything else
|
||||
defp do_replace_invalid(<<_, rest::bytes>>, rep, acc),
|
||||
do: do_replace_invalid(rest, rep, acc <> rep)
|
||||
|
||||
# Final
|
||||
defp do_replace_invalid(<<>>, _, acc), do: acc
|
||||
|
||||
@doc ~S"""
|
||||
Splits the string into chunks of characters that share a common trait.
|
||||
|
||||
@@ -2231,7 +2352,7 @@ defmodule String do
|
||||
If the first position is after the string ends or after
|
||||
the last position of the range, it returns an empty string:
|
||||
|
||||
iex> String.slice("elixir", 10..3)
|
||||
iex> String.slice("elixir", 10..3//1)
|
||||
""
|
||||
iex> String.slice("a", 1..1500)
|
||||
""
|
||||
@@ -2239,12 +2360,16 @@ defmodule String do
|
||||
"""
|
||||
@spec slice(t, Range.t()) :: t
|
||||
def slice(string, first..last//step = range) when is_binary(string) do
|
||||
# TODO: Deprecate negative steps on Elixir v1.16
|
||||
# TODO: Support negative steps as a reverse on Elixir v2.0.
|
||||
cond do
|
||||
step > 0 ->
|
||||
slice_range(string, first, last, step)
|
||||
|
||||
step == -1 and first > last ->
|
||||
IO.warn(
|
||||
"negative steps are not supported in String.slice/2, pass #{first}..#{last}//1 instead"
|
||||
)
|
||||
|
||||
slice_range(string, first, last, 1)
|
||||
|
||||
true ->
|
||||
@@ -2936,7 +3061,7 @@ defmodule String do
|
||||
defp codepoint_byte_size(_), do: 4
|
||||
|
||||
defp grapheme_to_binary(cp) when is_integer(cp), do: <<cp::utf8>>
|
||||
defp grapheme_to_binary(gc), do: :unicode.characters_to_binary(gc)
|
||||
defp grapheme_to_binary(gc) when is_list(gc), do: for(cp <- gc, do: <<cp::utf8>>, into: "")
|
||||
|
||||
defp grapheme_byte_size(cp) when is_integer(cp), do: codepoint_byte_size(cp)
|
||||
defp grapheme_byte_size(cps), do: grapheme_byte_size(cps, 0)
|
||||
|
||||
@@ -236,6 +236,27 @@ defmodule Supervisor do
|
||||
}
|
||||
end
|
||||
|
||||
Then the supervisor will call `Counter.start_link(arg)` to start the child
|
||||
process. This flow is summarized in the diagram below. Caller is a process
|
||||
which spawns the Supervisor process. The Supervisor then proceeds to call
|
||||
your code (Module) to spawn its child process:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Caller (Process)
|
||||
participant S as Supervisor (Process)
|
||||
participant M as Module (Code)
|
||||
|
||||
note right of C: child is a {module, arg} specification
|
||||
C->>+S: Supervisor.start_link([child])
|
||||
S-->>+M: module.child_spec(arg)
|
||||
M-->>-S: %{id: term, start: {module, :start_link, [arg]}}
|
||||
S-->>+M: module.start_link(arg)
|
||||
M->>M: Spawns child process (child_pid)
|
||||
M-->>-S: {:ok, child_pid} | :ignore | {:error, reason}
|
||||
S->>-C: {:ok, supervisor_pid} | {:error, reason}
|
||||
```
|
||||
|
||||
Luckily for us, `use GenServer` already defines a `Counter.child_spec/1`
|
||||
exactly like above, so you don't need to write the definition above yourself.
|
||||
If you want to customize the automatically generated `child_spec/1` function,
|
||||
@@ -392,10 +413,10 @@ defmodule Supervisor do
|
||||
The difference between the two approaches is that a module-based
|
||||
supervisor gives you more direct control over how the supervisor
|
||||
is initialized. Instead of calling `Supervisor.start_link/2` with
|
||||
a list of child specifications that are automatically initialized, we manually
|
||||
initialize the children by calling `Supervisor.init/2` inside its
|
||||
`c:init/1` callback. `Supervisor.init/2` accepts the same `:strategy`,
|
||||
`:max_restarts`, and `:max_seconds` options as `start_link/2`.
|
||||
a list of child specifications that are implicitly initialized for us,
|
||||
we must explicitly initialize the children by calling `Supervisor.init/2`
|
||||
inside its `c:init/1` callback. `Supervisor.init/2` accepts the same
|
||||
`:strategy`, `:max_restarts`, and `:max_seconds` options as `start_link/2`.
|
||||
|
||||
> #### `use Supervisor` {: .info}
|
||||
>
|
||||
@@ -537,13 +558,13 @@ defmodule Supervisor do
|
||||
{sup_flags(), [child_spec() | (old_erlang_child_spec :: :supervisor.child_spec())]}}
|
||||
| :ignore
|
||||
|
||||
@typedoc "Return values of `start_link` functions"
|
||||
@typedoc "Return values of `start_link/2` and `start_link/3`."
|
||||
@type on_start ::
|
||||
{:ok, pid}
|
||||
| :ignore
|
||||
| {:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
|
||||
@typedoc "Return values of `start_child` functions"
|
||||
@typedoc "Return values of `start_child/2`."
|
||||
@type on_start_child ::
|
||||
{:ok, child}
|
||||
| {:ok, child, info :: term}
|
||||
@@ -557,13 +578,13 @@ defmodule Supervisor do
|
||||
"""
|
||||
@type child :: pid | :undefined
|
||||
|
||||
@typedoc "The supervisor name"
|
||||
@typedoc "The supervisor name."
|
||||
@type name :: atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@typedoc "Option values used by the `start*` functions"
|
||||
@typedoc "Option values used by the `start_link/2` and `start_link/3` functions."
|
||||
@type option :: {:name, name}
|
||||
|
||||
@typedoc "The supervisor flags returned on init"
|
||||
@typedoc "The supervisor flags returned on init."
|
||||
@type sup_flags() :: %{
|
||||
strategy: strategy(),
|
||||
intensity: non_neg_integer(),
|
||||
@@ -571,32 +592,32 @@ defmodule Supervisor do
|
||||
auto_shutdown: auto_shutdown()
|
||||
}
|
||||
|
||||
@typedoc "The supervisor reference"
|
||||
@typedoc "The supervisor reference."
|
||||
@type supervisor :: pid | name | {atom, node}
|
||||
|
||||
@typedoc "Options given to `start_link/2` and `init/2`"
|
||||
@typedoc "Options given to `start_link/2` and `c:init/1`."
|
||||
@type init_option ::
|
||||
{:strategy, strategy}
|
||||
| {:max_restarts, non_neg_integer}
|
||||
| {:max_seconds, pos_integer}
|
||||
| {:auto_shutdown, auto_shutdown}
|
||||
|
||||
@typedoc "Supported restart options"
|
||||
@typedoc "Supported restart options."
|
||||
@type restart :: :permanent | :transient | :temporary
|
||||
|
||||
@typedoc "Supported shutdown options"
|
||||
@typedoc "Supported shutdown options."
|
||||
@type shutdown :: timeout() | :brutal_kill
|
||||
|
||||
@typedoc "Supported strategies"
|
||||
@typedoc "Supported strategies."
|
||||
@type strategy :: :one_for_one | :one_for_all | :rest_for_one
|
||||
|
||||
@typedoc "Supported automatic shutdown options"
|
||||
@typedoc "Supported automatic shutdown options."
|
||||
@type auto_shutdown :: :never | :any_significant | :all_significant
|
||||
|
||||
@typedoc """
|
||||
Supervisor type.
|
||||
Type of a supervised child.
|
||||
|
||||
Whether the supervisor is a worker or a supervisor.
|
||||
Whether the supervised child is a worker or a supervisor.
|
||||
"""
|
||||
@type type :: :worker | :supervisor
|
||||
|
||||
@@ -616,18 +637,39 @@ defmodule Supervisor do
|
||||
optional(:significant) => boolean()
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
A module-based child spec.
|
||||
|
||||
This is a form of child spec that you can pass to functions such as `child_spec/2`,
|
||||
`start_child/2`, and `start_link/2`, in addition to the normalized `t:child_spec/0`.
|
||||
|
||||
A module-based child spec can be:
|
||||
|
||||
* a **module** — the supervisor calls `module.child_spec([])` to retrieve the
|
||||
child specification
|
||||
|
||||
* a **two-element tuple** in the shape of `{module, arg}` — the supervisor
|
||||
calls `module.child_spec(arg)` to retrieve the child specification
|
||||
|
||||
"""
|
||||
@typedoc since: "1.16.0"
|
||||
@type module_spec :: {module(), args :: term()} | module()
|
||||
|
||||
@doc """
|
||||
Starts a supervisor with the given children.
|
||||
|
||||
`children` is a list of the following forms:
|
||||
|
||||
* a [child specification](`t:child_spec/0`)
|
||||
* a child specification (see `t:child_spec/0`)
|
||||
|
||||
* a module, where `module.child_spec([])` will be invoked to retrieve
|
||||
its child specification
|
||||
* a module, where the supervisor calls `module.child_spec([])`
|
||||
to retrieve the child specification (see `t:module_spec/0`)
|
||||
|
||||
* a two-element tuple in the shape of `{module, arg}`, where `module.child_spec(arg)`
|
||||
will be invoked to retrieve its child specification
|
||||
* a `{module, arg}` tuple, where the supervisor calls `module.child_spec(arg)`
|
||||
to retrieve the child specification (see `t:module_spec/0`)
|
||||
|
||||
* a (old) Erlang-style child specification (see
|
||||
[`:supervisor.child_spec()`](`t::supervisor.child_spec/0`))
|
||||
|
||||
A strategy is required to be provided through the `:strategy` option. See
|
||||
"Supervisor strategies and options" for examples and other options.
|
||||
@@ -654,14 +696,10 @@ defmodule Supervisor do
|
||||
with `:normal` reason.
|
||||
"""
|
||||
@spec start_link(
|
||||
[
|
||||
child_spec()
|
||||
| {module, term}
|
||||
| module
|
||||
| (old_erlang_child_spec :: :supervisor.child_spec())
|
||||
],
|
||||
[child_spec | module_spec | (old_erlang_child_spec :: :supervisor.child_spec())],
|
||||
[option | init_option]
|
||||
) :: {:ok, pid} | {:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
) ::
|
||||
{:ok, pid} | {:error, {:already_started, pid} | {:shutdown, term} | term}
|
||||
def start_link(children, options) when is_list(children) do
|
||||
{sup_opts, start_opts} =
|
||||
Keyword.split(options, [:strategy, :max_seconds, :max_restarts, :auto_shutdown])
|
||||
@@ -709,12 +747,7 @@ defmodule Supervisor do
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec init(
|
||||
[
|
||||
child_spec()
|
||||
| {module, term}
|
||||
| module
|
||||
| (old_erlang_child_spec :: :supervisor.child_spec())
|
||||
],
|
||||
[child_spec | module_spec | (old_erlang_child_spec :: :supervisor.child_spec())],
|
||||
[init_option]
|
||||
) ::
|
||||
{:ok,
|
||||
@@ -842,7 +875,7 @@ defmodule Supervisor do
|
||||
`module.child_spec([])`.
|
||||
|
||||
After the child specification is retrieved, the fields on `overrides`
|
||||
are directly applied on the child spec. If `overrides` has keys that
|
||||
are directly applied to the child spec. If `overrides` has keys that
|
||||
do not map to any child specification field, an error is raised.
|
||||
|
||||
See the "Child specification" section in the module documentation
|
||||
@@ -859,7 +892,7 @@ defmodule Supervisor do
|
||||
#=> start: {Agent, :start_link, [fn -> :ok end]}}
|
||||
|
||||
"""
|
||||
@spec child_spec(child_spec() | {module, arg :: term} | module, keyword) :: child_spec()
|
||||
@spec child_spec(child_spec() | module_spec(), keyword()) :: child_spec()
|
||||
def child_spec(module_or_map, overrides)
|
||||
|
||||
def child_spec({_, _, _, _, _, _} = tuple, _overrides) do
|
||||
@@ -956,12 +989,8 @@ defmodule Supervisor do
|
||||
"""
|
||||
@spec start_child(
|
||||
supervisor,
|
||||
child_spec()
|
||||
| {module, term}
|
||||
| module
|
||||
| (old_erlang_child_spec :: :supervisor.child_spec())
|
||||
) ::
|
||||
on_start_child
|
||||
child_spec | module_spec | (old_erlang_child_spec :: :supervisor.child_spec())
|
||||
) :: on_start_child
|
||||
def start_child(supervisor, {_, _, _, _, _, _} = child_spec) do
|
||||
call(supervisor, {:start_child, child_spec})
|
||||
end
|
||||
|
||||
@@ -194,7 +194,7 @@ defmodule Supervisor.Spec do
|
||||
defp assert_unique_ids([id | rest]) do
|
||||
if id in rest do
|
||||
raise ArgumentError,
|
||||
"duplicated ID #{inspect(id)} found in the supervisor specification, " <>
|
||||
"duplicate ID #{inspect(id)} found in the supervisor specification, " <>
|
||||
"please explicitly pass the :id option when defining this worker/supervisor"
|
||||
else
|
||||
assert_unique_ids(rest)
|
||||
|
||||
+13
-23
@@ -64,6 +64,12 @@ defmodule System do
|
||||
"""
|
||||
|
||||
defmodule EnvError do
|
||||
@moduledoc """
|
||||
An exception raised when a system environment variable is not set.
|
||||
|
||||
For example, see `System.fetch_env!/1`.
|
||||
"""
|
||||
|
||||
defexception [:env]
|
||||
|
||||
@impl true
|
||||
@@ -749,9 +755,12 @@ defmodule System do
|
||||
Sets multiple environment variables.
|
||||
|
||||
Sets a new value for each environment variable corresponding
|
||||
to each `{key, value}` pair in `enum`. Keys are automatically
|
||||
converted to strings, values are sent as is. `nil` values erase
|
||||
to each `{key, value}` pair in `enum`. Keys and non-nil values
|
||||
are automatically converted to charlists. `nil` values erase
|
||||
the given keys.
|
||||
|
||||
Overall, this is a convenience wrapper around `put_env/2` and
|
||||
`delete_env/2` with support for different key and value formats.
|
||||
"""
|
||||
@spec put_env(Enumerable.t()) :: :ok
|
||||
def put_env(enum) do
|
||||
@@ -996,25 +1005,6 @@ defmodule System do
|
||||
`Port` module describes this problem and possible solutions under
|
||||
the "Zombie processes" section.
|
||||
|
||||
> #### Windows argument splitting and untrusted arguments {: .warning}
|
||||
>
|
||||
> On Unix systems, arguments are passed to a new operating system
|
||||
> process as an array of strings but on Windows it is up to the child
|
||||
> process to parse them and some Windows programs may apply their own
|
||||
> rules, which are inconsistent with the standard C runtime `argv` parsing
|
||||
>
|
||||
> This is particularly troublesome when invoking `.bat` or `.com` files
|
||||
> as these run implicitly through `cmd.exe`, whose argument parsing is
|
||||
> vulnerable to malicious input and can be used to run arbitrary shell
|
||||
> commands.
|
||||
>
|
||||
> Therefore, if you are running on Windows and you execute batch
|
||||
> files or `.com` applications, you must not pass untrusted input as
|
||||
> arguments to the program. You may avoid accidentally executing them
|
||||
> by explicitly passing the extension of the program you want to run,
|
||||
> such as `.exe`, and double check the program is indeed not a batch
|
||||
> file or `.com` application.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> System.cmd("echo", ["hello"])
|
||||
@@ -1059,8 +1049,8 @@ defmodule System do
|
||||
* `:parallelism` - when `true`, the VM will schedule port tasks to improve
|
||||
parallelism in the system. If set to `false`, the VM will try to perform
|
||||
commands immediately, improving latency at the expense of parallelism.
|
||||
The default is `false`, and can be set on system startup by passing the
|
||||
[`+spp`](https://www.erlang.org/doc/man/erl.html#+spp) flag to `--erl`.
|
||||
The default is `false`, and can be set on system startup by passing the
|
||||
[`+spp`](https://www.erlang.org/doc/man/erl.html#+spp) flag to `--erl`.
|
||||
Use `:erlang.system_info(:port_parallelism)` to check if enabled.
|
||||
|
||||
## Error reasons
|
||||
|
||||
+51
-13
@@ -44,10 +44,38 @@ defmodule Task do
|
||||
means that, if the caller crashes, the task will crash
|
||||
too and vice-versa. This is on purpose: if the process
|
||||
meant to receive the result no longer exists, there is
|
||||
no purpose in completing the computation.
|
||||
no purpose in completing the computation. If this is not
|
||||
desired, you will want to use supervised tasks, described
|
||||
in a subsequent section.
|
||||
|
||||
If this is not desired, you will want to use supervised
|
||||
tasks, described next.
|
||||
## Tasks are processes
|
||||
|
||||
Tasks are processes and so data will need to be completely copied
|
||||
to them. Take the following code as an example:
|
||||
|
||||
large_data = fetch_large_data()
|
||||
task = Task.async(fn -> do_some_work(large_data) end)
|
||||
res = do_some_other_work()
|
||||
res + Task.await(task)
|
||||
|
||||
The code above copies over all of `large_data`, which can be
|
||||
resource intensive depending on the size of the data.
|
||||
There are two ways to address this.
|
||||
|
||||
First, if you need to access only part of `large_data`,
|
||||
consider extracting it before the task:
|
||||
|
||||
large_data = fetch_large_data()
|
||||
subset_data = large_data.some_field
|
||||
task = Task.async(fn -> do_some_work(subset_data) end)
|
||||
|
||||
Alternatively, if you can move the data loading altogether
|
||||
to the task, it may be even better:
|
||||
|
||||
task = Task.async(fn ->
|
||||
large_data = fetch_large_data()
|
||||
do_some_work(large_data)
|
||||
end)
|
||||
|
||||
## Dynamically supervised tasks
|
||||
|
||||
@@ -1109,14 +1137,15 @@ defmodule Task do
|
||||
* `{:ok, term}` if the task has successfully reported its
|
||||
result back in the given time interval
|
||||
* `{:exit, reason}` if the task has died
|
||||
* `nil` if the task keeps running past the timeout
|
||||
* `nil` if the task keeps running, either because a limit
|
||||
has been reached or past the timeout
|
||||
|
||||
Check `yield/2` for more information.
|
||||
|
||||
## Example
|
||||
|
||||
`Task.yield_many/2` allows developers to spawn multiple tasks
|
||||
and retrieve the results received in a given timeframe.
|
||||
and retrieve the results received in a given time frame.
|
||||
If we combine it with `Task.shutdown/2` (or `Task.ignore/1`),
|
||||
it allows us to gather those results and cancel (or ignore)
|
||||
the tasks that have not replied in time.
|
||||
@@ -1162,6 +1191,10 @@ defmodule Task do
|
||||
The second argument is either a timeout or options, which defaults
|
||||
to this:
|
||||
|
||||
* `:limit` - the maximum amount of tasks to wait for.
|
||||
If the limit is reached before the timeout, this function
|
||||
returns immediately without triggering the `:on_timeout` behaviour
|
||||
|
||||
* `:timeout` - the maximum amount of time (in milliseconds or `:infinity`)
|
||||
each task is allowed to execute for. Defaults to `5000`.
|
||||
|
||||
@@ -1173,7 +1206,11 @@ defmodule Task do
|
||||
* `:kill_task` - the task that timed out is killed.
|
||||
"""
|
||||
@spec yield_many([t], timeout) :: [{t, {:ok, term} | {:exit, term} | nil}]
|
||||
@spec yield_many([t], timeout: timeout, on_timeout: :nothing | :ignore | :kill_task) ::
|
||||
@spec yield_many([t],
|
||||
limit: pos_integer(),
|
||||
timeout: timeout,
|
||||
on_timeout: :nothing | :ignore | :kill_task
|
||||
) ::
|
||||
[{t, {:ok, term} | {:exit, term} | nil}]
|
||||
def yield_many(tasks, opts \\ [])
|
||||
|
||||
@@ -1182,9 +1219,6 @@ defmodule Task do
|
||||
end
|
||||
|
||||
def yield_many(tasks, opts) when is_list(opts) do
|
||||
on_timeout = Keyword.get(opts, :on_timeout, :nothing)
|
||||
timeout = Keyword.get(opts, :timeout, 5_000)
|
||||
|
||||
refs =
|
||||
Map.new(tasks, fn %Task{ref: ref, owner: owner} = task ->
|
||||
if owner != self() do
|
||||
@@ -1194,6 +1228,9 @@ defmodule Task do
|
||||
{ref, nil}
|
||||
end)
|
||||
|
||||
on_timeout = Keyword.get(opts, :on_timeout, :nothing)
|
||||
timeout = Keyword.get(opts, :timeout, 5_000)
|
||||
limit = Keyword.get(opts, :limit, map_size(refs))
|
||||
timeout_ref = make_ref()
|
||||
|
||||
timer_ref =
|
||||
@@ -1202,16 +1239,17 @@ defmodule Task do
|
||||
end
|
||||
|
||||
try do
|
||||
yield_many(map_size(refs), refs, timeout_ref, timer_ref)
|
||||
yield_many(limit, refs, timeout_ref, timer_ref)
|
||||
catch
|
||||
{:noconnection, reason} ->
|
||||
exit({reason, {__MODULE__, :yield_many, [tasks, timeout]}})
|
||||
else
|
||||
refs ->
|
||||
{timed_out?, refs} ->
|
||||
for task <- tasks do
|
||||
value =
|
||||
with nil <- Map.fetch!(refs, task.ref) do
|
||||
case on_timeout do
|
||||
_ when not timed_out? -> nil
|
||||
:nothing -> nil
|
||||
:kill_task -> shutdown(task, :brutal_kill)
|
||||
:ignore -> ignore(task)
|
||||
@@ -1226,7 +1264,7 @@ defmodule Task do
|
||||
defp yield_many(0, refs, timeout_ref, timer_ref) do
|
||||
timer_ref && Process.cancel_timer(timer_ref)
|
||||
receive do: (^timeout_ref -> :ok), after: (0 -> :ok)
|
||||
refs
|
||||
{false, refs}
|
||||
end
|
||||
|
||||
defp yield_many(limit, refs, timeout_ref, timer_ref) do
|
||||
@@ -1243,7 +1281,7 @@ defmodule Task do
|
||||
end
|
||||
|
||||
^timeout_ref ->
|
||||
refs
|
||||
{true, refs}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -338,6 +338,11 @@ defmodule Task.Supervisor do
|
||||
* `:kill_task` - the task that timed out is killed. The value
|
||||
emitted for that task is `{:exit, :timeout}`.
|
||||
|
||||
* `:zip_input_on_exit` - (since v1.14.0) adds the original
|
||||
input to `:exit` tuples. The value emitted for that task is
|
||||
`{:exit, {input, reason}}`, where `input` is the collection element
|
||||
that caused an exited during processing. Defaults to `false`.
|
||||
|
||||
* `:shutdown` - `:brutal_kill` if the tasks must be killed directly on shutdown
|
||||
or an integer indicating the timeout value. Defaults to `5000` milliseconds.
|
||||
The tasks must trap exits for the timeout to have an effect.
|
||||
|
||||
+24
-16
@@ -38,6 +38,12 @@ defmodule URI do
|
||||
@opaque authority :: nil | binary
|
||||
|
||||
defmodule Error do
|
||||
@moduledoc """
|
||||
An exception raised when an error occurs when a `URI` is invalid.
|
||||
|
||||
For example, see `URI.new!/1`.
|
||||
"""
|
||||
|
||||
defexception [:action, :reason, :part]
|
||||
|
||||
@doc false
|
||||
@@ -356,22 +362,24 @@ defmodule URI do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Percent-escapes all characters that require escaping in `string`.
|
||||
Percent-encodes all characters that require escaping in `string`.
|
||||
|
||||
This means reserved characters, such as `:` and `/`, and the
|
||||
so-called unreserved characters, which have the same meaning both
|
||||
escaped and unescaped, won't be escaped by default.
|
||||
By default, this function is meant to escape the whole URI, and
|
||||
therefore it will escape all characters which are foreign to the
|
||||
URI specification. Reserved characters (such as `:` and `/`) or
|
||||
unreserved (such as letters and numbers) are not escaped.
|
||||
|
||||
Because different components of a URI require different escaping
|
||||
rules, this function also accepts a `predicate` function as an optional
|
||||
argument. If passed, this function will be called with each byte
|
||||
in `string` as its argument and should return a truthy value (anything other
|
||||
than `false` or `nil`) if the given byte should be left as is, or
|
||||
return a falsy value (`false` or `nil`) if the character should be
|
||||
escaped. Defaults to `URI.char_unescaped?/1`.
|
||||
|
||||
See `encode_www_form/1` if you are interested in escaping reserved
|
||||
characters too.
|
||||
|
||||
This function also accepts a `predicate` function as an optional
|
||||
argument. If passed, this function will be called with each byte
|
||||
in `string` as its argument and should return a truthy value (anything other
|
||||
than `false` or `nil`) if the given byte should be left as is, or return a
|
||||
falsy value (`false` or `nil`) if the character should be escaped. Defaults
|
||||
to `URI.char_unescaped?/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> URI.encode("ftp://s-ite.tld/?value=put it+й")
|
||||
@@ -648,16 +656,16 @@ defmodule URI do
|
||||
scheme = String.downcase(scheme, :ascii)
|
||||
|
||||
case map do
|
||||
%{port: port} when is_integer(port) ->
|
||||
%{port: port} when port != :undefined ->
|
||||
%{uri | scheme: scheme}
|
||||
|
||||
%{} ->
|
||||
%{uri | scheme: scheme, port: default_port(scheme)}
|
||||
case default_port(scheme) do
|
||||
nil -> %{uri | scheme: scheme}
|
||||
port -> %{uri | scheme: scheme, port: port}
|
||||
end
|
||||
end
|
||||
|
||||
%{port: :undefined} ->
|
||||
%{uri | port: nil}
|
||||
|
||||
%{} ->
|
||||
uri
|
||||
end
|
||||
|
||||
@@ -217,6 +217,12 @@ defmodule Version do
|
||||
end
|
||||
|
||||
defmodule InvalidRequirementError do
|
||||
@moduledoc """
|
||||
An exception raised when a version requirement is invalid.
|
||||
|
||||
For example, see `Version.parse_requirement!/1`.
|
||||
"""
|
||||
|
||||
defexception [:requirement]
|
||||
|
||||
@impl true
|
||||
@@ -231,6 +237,12 @@ defmodule Version do
|
||||
end
|
||||
|
||||
defmodule InvalidVersionError do
|
||||
@moduledoc """
|
||||
An exception raised when a version is invalid.
|
||||
|
||||
For example, see `Version.parse!/1`.
|
||||
"""
|
||||
|
||||
defexception [:version]
|
||||
|
||||
@impl true
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
# Code-related anti-patterns
|
||||
|
||||
This document outlines potential anti-patterns related to your code and particular Elixir idioms and features.
|
||||
|
||||
## Comments overuse
|
||||
|
||||
#### Problem
|
||||
|
||||
When you overuse comments or comment self-explanatory code, it can have the effect of making code *less readable*.
|
||||
|
||||
#### Example
|
||||
|
||||
```elixir
|
||||
# Returns the Unix timestamp of 5 minutes from the current time
|
||||
defp unix_five_min_from_now do
|
||||
# Get the current time
|
||||
now = DateTime.utc_now()
|
||||
|
||||
# Convert it to a Unix timestamp
|
||||
unix_now = DateTime.to_unix(now, :second)
|
||||
|
||||
# Add five minutes in seconds
|
||||
unix_now + (60 * 5)
|
||||
end
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
Prefer clear and self-explanatory function names, module names, and variable names when possible. In the example above, the function name explains well what the function does, so you likely won't need the comment before it. The code also explains the operations well through variable names and clear function calls.
|
||||
|
||||
You could refactor the code above like this:
|
||||
|
||||
```elixir
|
||||
@five_min_in_seconds 60 * 5
|
||||
|
||||
defp unix_five_min_from_now do
|
||||
now = DateTime.utc_now()
|
||||
unix_now = DateTime.to_unix(now, :second)
|
||||
unix_now + @five_min_in_seconds
|
||||
end
|
||||
```
|
||||
|
||||
We removed the unnecessary comments. We also added a `@five_min_in_seconds` module attribute, which serves the additional purpose of giving a name to the "magic" number `60 * 5`, making the code clearer and more expressive.
|
||||
|
||||
#### Additional remarks
|
||||
|
||||
Elixir makes a clear distinction between **documentation** and code comments. The language has built-in first-class support for documentation through `@doc`, `@moduledoc`, and more. See the ["Writing documentation"](../getting-started/writing-documentation.md) guide for more information.
|
||||
|
||||
## Complex `else` clauses in `with`
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern refers to `with` statements that flatten all its error clauses into a single complex `else` block. This situation is harmful to the code readability and maintainability because it's difficult to know from which clause the error value came.
|
||||
|
||||
#### Example
|
||||
|
||||
An example of this anti-pattern, as shown below, is a function `open_decoded_file/1` that reads a Base64-encoded string content from a file and returns a decoded binary string. This function uses a `with` statement that needs to handle two possible errors, all of which are concentrated in a single complex `else` block.
|
||||
|
||||
```elixir
|
||||
def open_decoded_file(path) do
|
||||
with {:ok, encoded} <- File.read(path),
|
||||
{:ok, decoded} <- Base.decode64(encoded) do
|
||||
{:ok, String.trim(decoded)}
|
||||
else
|
||||
{:error, _} -> {:error, :badfile}
|
||||
:error -> {:error, :badencoding}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In the code above, it is unclear how each pattern on the left side of `<-` relates to their error at the end. The more patterns in a `with`, the less clear the code gets, and the more likely it is that unrelated failures will overlap each other.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
In this situation, instead of concentrating all error handling within a single complex `else` block, it is better to normalize the return types in specific private functions. In this way, `with` can focus on the success case and the errors are normalized closer to where they happen, leading to better organized and maintainable code.
|
||||
|
||||
```elixir
|
||||
def open_decoded_file(path) do
|
||||
with {:ok, encoded} <- file_read(path),
|
||||
{:ok, decoded} <- base_decode64(encoded) do
|
||||
{:ok, String.trim(decoded)}
|
||||
end
|
||||
end
|
||||
|
||||
defp file_read(path) do
|
||||
case File.read(path) do
|
||||
{:ok, contents} -> {:ok, contents}
|
||||
{:error, _} -> {:error, :badfile}
|
||||
end
|
||||
end
|
||||
|
||||
defp base_decode64(contents) do
|
||||
case Base.decode64(contents) do
|
||||
{:ok, decoded} -> {:ok, decoded}
|
||||
:error -> {:error, :badencoding}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Complex extractions in clauses
|
||||
|
||||
#### Problem
|
||||
|
||||
When we use multi-clause functions, it is possible to extract values in the clauses for further usage and for pattern matching/guard checking. This extraction itself does not represent an anti-pattern, but when you have *extractions made across several clauses and several arguments of the same function*, it becomes hard to know which extracted parts are used for pattern/guards and what is used only inside the function body. This anti-pattern is related to [Unrelated multi-clause function](design-anti-patterns.md#unrelated-multi-clause-function), but with implications of its own. It impairs the code readability in a different way.
|
||||
|
||||
#### Example
|
||||
|
||||
The multi-clause function `drive/1` is extracting fields of an `%User{}` struct for usage in the clause expression (`age`) and for usage in the function body (`name`):
|
||||
|
||||
```elixir
|
||||
def drive(%User{name: name, age: age}) when age >= 18 do
|
||||
"#{name} can drive"
|
||||
end
|
||||
|
||||
def drive(%User{name: name, age: age}) when age < 18 do
|
||||
"#{name} cannot drive"
|
||||
end
|
||||
```
|
||||
|
||||
While the example above is small and does not configure an anti-pattern, it is an example of mixed extraction and pattern matching. A situation where `drive/1` was more complex, having many more clauses, arguments, and extractions, would make it hard to know at a glance which variables are used for pattern/guards and which ones are not.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
As shown below, a possible solution to this anti-pattern is to extract only pattern/guard related variables in the signature once you have many arguments or multiple clauses:
|
||||
|
||||
```elixir
|
||||
def drive(%User{age: age} = user) when age >= 18 do
|
||||
%User{name: name} = user
|
||||
"#{name} can drive"
|
||||
end
|
||||
|
||||
def drive(%User{age: age} = user) when age < 18 do
|
||||
%User{name: name} = user
|
||||
"#{name} cannot drive"
|
||||
end
|
||||
```
|
||||
|
||||
## Dynamic atom creation
|
||||
|
||||
#### Problem
|
||||
|
||||
An `Atom` is an Elixir basic type whose value is its own name. Atoms are often useful to identify resources or express the state, or result, of an operation. Creating atoms dynamically is not an anti-pattern by itself; however, atoms are not garbage collected by the Erlang Virtual Machine, so values of this type live in memory during a software's entire execution lifetime. The Erlang VM limits the number of atoms that can exist in an application by default to *1_048_576*, which is more than enough to cover all atoms defined in a program, but attempts to serve as an early limit for applications which are "leaking atoms" through dynamic creation.
|
||||
|
||||
For these reason, creating atoms dynamically can be considered an anti-pattern when the developer has no control over how many atoms will be created during the software execution. This unpredictable scenario can expose the software to unexpected behaviour caused by excessive memory usage, or even by reaching the maximum number of *atoms* possible.
|
||||
|
||||
#### Example
|
||||
|
||||
Picture yourself implementing code that converts string values into atoms. These strings could have been received from an external system, either as part of a request into our application, or as part of a response to your application. This dynamic and unpredictable scenario poses a security risk, as these uncontrolled conversions can potentially trigger out-of-memory errors.
|
||||
|
||||
```elixir
|
||||
defmodule MyRequestHandler do
|
||||
def parse(%{"status" => status, "message" => message} = _payload) do
|
||||
%{status: String.to_atom(status), message: message}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> MyRequestHandler.parse(%{"status" => "ok", "message" => "all good"})
|
||||
%{status: :ok, message: "all good"}
|
||||
```
|
||||
|
||||
When we use the `String.to_atom/1` function to dynamically create an atom, it essentially gains potential access to create arbitrary atoms in our system, causing us to lose control over adhering to the limits established by the BEAM. This issue could be exploited by someone to create enough atoms to shut down a system.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To eliminate this anti-pattern, developers must either perform explicit conversions by mapping strings to atoms or replace the use of `String.to_atom/1` with `String.to_existing_atom/1`. An explicit conversion could be done as follows:
|
||||
|
||||
```elixir
|
||||
defmodule MyRequestHandler do
|
||||
def parse(%{"status" => status, "message" => message} = _payload) do
|
||||
%{status: convert_status(status), message: message}
|
||||
end
|
||||
|
||||
defp convert_status("ok"), do: :ok
|
||||
defp convert_status("error"), do: :error
|
||||
defp convert_status("redirect"), do: :redirect
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> MyRequestHandler.parse(%{"status" => "status_not_seen_anywhere", "message" => "all good"})
|
||||
** (FunctionClauseError) no function clause matching in MyRequestHandler.convert_status/1
|
||||
```
|
||||
|
||||
By explicitly listing all supported statuses, you guarantee only a limited number of conversions may happen. Passing an invalid status will lead to a function clause error.
|
||||
|
||||
An alternative is to use `String.to_existing_atom/1`, which will only convert a string to atom if the atom already exists in the system:
|
||||
|
||||
```elixir
|
||||
defmodule MyRequestHandler do
|
||||
def parse(%{"status" => status, "message" => message} = _payload) do
|
||||
%{status: String.to_existing_atom(status), message: message}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> MyRequestHandler.parse(%{"status" => "status_not_seen_anywhere", "message" => "all good"})
|
||||
** (ArgumentError) errors were found at the given arguments:
|
||||
|
||||
* 1st argument: not an already existing atom
|
||||
```
|
||||
|
||||
In such cases, passing an unknown status will raise as long as the status was not defined anywhere as an atom in the system. However, assuming `status` can be either `:ok`, `:error`, or `:redirect`, how can you guarantee those atoms exist? You must ensure those atoms exist somewhere **in the same module** where `String.to_existing_atom/1` is called. For example, if you had this code:
|
||||
|
||||
```elixir
|
||||
defmodule MyRequestHandler do
|
||||
def parse(%{"status" => status, "message" => message} = _payload) do
|
||||
%{status: String.to_existing_atom(status), message: message}
|
||||
end
|
||||
|
||||
def handle(%{status: status}) do
|
||||
case status do
|
||||
:ok -> ...
|
||||
:error -> ...
|
||||
:redirect -> ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
All valid statuses are defined as atoms within the same module, and that's enough. If you want to be explicit, you could also have a function that lists them:
|
||||
|
||||
```elixir
|
||||
def valid_statuses do
|
||||
[:ok, :error, :redirect]
|
||||
end
|
||||
```
|
||||
|
||||
However, keep in mind using a module attribute or defining the atoms in the module body, outside of a function, are not sufficient, as the module body is only executed during compilation and it is not necessarily part of the compiled module loaded at runtime.
|
||||
|
||||
## Long parameter list
|
||||
|
||||
#### Problem
|
||||
|
||||
In a functional language like Elixir, functions tend to explicitly receive all inputs and return all relevant outputs, instead of relying on mutations or side-effects. As functions grow in complexity, the amount of arguments (parameters) they need to work with may grow, to a point where the function's interface becomes confusing and prone to errors during use.
|
||||
|
||||
#### Example
|
||||
|
||||
In the following example, the `loan/6` functions takes too many arguments, causing its interface to be confusing and potentially leading developers to introduce errors during calls to this function.
|
||||
|
||||
```elixir
|
||||
defmodule Library do
|
||||
# Too many parameters that can be grouped!
|
||||
def loan(user_name, email, password, user_alias, book_title, book_ed) do
|
||||
...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To address this anti-pattern, related arguments can be grouped using key-value data structures, such as maps, structs, or even keyword lists in the case of optional arguments. This effectively reduces the number of arguments and the key-value data structures adds clarity to the caller.
|
||||
|
||||
For this particular example, the arguments to `loan/6` can be grouped into two different maps, thereby reducing its arity to `loan/2`:
|
||||
|
||||
```elixir
|
||||
defmodule Library do
|
||||
def loan(%{name: name, email: email, password: password, alias: alias} = user, %{title: title, ed: ed} = book) do
|
||||
...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In some cases, the function with too many arguments may be a private function, which gives us more flexibility over how to separate the function arguments. One possible suggestion for such scenarios is to split the arguments in two maps (or tuples): one map keeps the data that may change, and the other keeps the data that won't change (read-only). This gives us a mechanical option to refactor the code.
|
||||
|
||||
Other times, a function may legitimately take half a dozen or more completely unrelated arguments. This may suggest the function is trying to do too much and would be better broken into multiple functions, each responsible for a smaller piece of the overall responsibility.
|
||||
|
||||
## Namespace trespassing
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern manifests when a package author or a library defines modules outside of its "namespace". A library should use its name as a "prefix" for all of its modules. For example, a package named `:my_lib` should define all of its modules within the `MyLib` namespace, such as `MyLib.User`, `MyLib.SubModule`, `MyLib.Application`, and `MyLib` itself.
|
||||
|
||||
This is important because the Erlang VM can only load one instance of a module at a time. So if there are multiple libraries that define the same module, then they are incompatible with each other due to this limitation. By always using the library name as a prefix, it avoids module name clashes due to the unique prefix.
|
||||
|
||||
#### Example
|
||||
|
||||
This problem commonly manifests when writing an extension of another library. For example, imagine you are writing a package that adds authentication to [Plug](https://github.com/elixir-plug/plug) called `:plug_auth`. You must avoid defining modules within the `Plug` namespace:
|
||||
|
||||
```elixir
|
||||
defmodule Plug.Auth do
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
Even if `Plug` does not currently define a `Plug.Auth` module, it may add such a module in the future, which would ultimately conflict with `plug_auth`'s definition.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
Given the package is named `:plug_auth`, it must define modules inside the `PlugAuth` namespace:
|
||||
|
||||
```elixir
|
||||
defmodule PlugAuth do
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
#### Additional remarks
|
||||
|
||||
There are few known exceptions to this anti-pattern:
|
||||
|
||||
* [Protocol implementations](`Kernel.defimpl/2`) are, by design, defined under the protocol namespace
|
||||
|
||||
* In some scenarios, the namespace owner may allow exceptions to this rule. For example, in Elixir itself, you defined [custom Mix tasks](`Mix.Task`) by placing them under the `Mix.Tasks` namespace, such as `Mix.Tasks.PlugAuth`
|
||||
|
||||
* If you are the maintainer for both `plug` and `plug_auth`, then you may allow `plug_auth` to define modules with the `Plug` namespace, such as `Plug.Auth`. However, you are responsible for avoiding or managing any conflicts that may arise in the future
|
||||
|
||||
## Non-assertive map access
|
||||
|
||||
#### Problem
|
||||
|
||||
In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically.
|
||||
|
||||
When a key is expected to exist in a map, it must be accessed using the `map.key` notation, making it clear to developers (and the compiler) that the key must exist. If the key does not exist, an exception is raised (and in some cases also compiler warnings). This is also known as the static notation, as the key is known at the time of writing the code.
|
||||
|
||||
When a key is optional, the `map[:key]` notation must be used instead. This way, if the informed key does not exist, `nil` is returned. This is the dynamic notation, as it also supports dynamic key access, such as `map[some_var]`.
|
||||
|
||||
When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpectedly missing, you will have a `nil` value propagate through the system, instead of raising on map access.
|
||||
|
||||
#### Example
|
||||
|
||||
The function `plot/1` tries to draw a graphic to represent the position of a point in a cartesian plane. This function receives a parameter of `Map` type with the point attributes, which can be a point of a 2D or 3D cartesian coordinate system. This function uses dynamic access to retrieve values for the map keys:
|
||||
|
||||
```elixir
|
||||
defmodule Graphics do
|
||||
def plot(point) do
|
||||
# Some other code...
|
||||
{point[:x], point[:y], point[:z]}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> point_2d = %{x: 2, y: 3}
|
||||
%{x: 2, y: 3}
|
||||
iex> point_3d = %{x: 5, y: 6, z: 7}
|
||||
%{x: 5, y: 6, z: 7}
|
||||
iex> Graphics.plot(point_2d)
|
||||
{2, 3, nil}
|
||||
iex> Graphics.plot(point_3d)
|
||||
{5, 6, 7}
|
||||
```
|
||||
|
||||
Given we want to plot both 2D and 3D points, the behaviour above is expected. But what happens if we forget to pass a point with either `:x` or `:y`?
|
||||
|
||||
```elixir
|
||||
iex> bad_point = %{y: 3, z: 4}
|
||||
%{y: 3, z: 4}
|
||||
iex> Graphics.plot(bad_point)
|
||||
{nil, 3, 4}
|
||||
```
|
||||
|
||||
The behaviour above is unexpected because our function should not work with points without a `:x` key. This leads to subtle bugs, as we may now pass `nil` to another function, instead of raising early on.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern, we must use the dynamic `map[:key]` syntax and the static `map.key` notation according to our requirements. We expect `:x` and `:y` to always exist, but not `:z`. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern:
|
||||
|
||||
```elixir
|
||||
defmodule Graphics do
|
||||
def plot(point) do
|
||||
# Some other code...
|
||||
{point.x, point.y, point[:z]}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> Graphics.plot(point_2d)
|
||||
{2, 3, nil}
|
||||
iex> Graphics.plot(bad_point)
|
||||
** (KeyError) key :x not found in: %{y: 3, z: 4} # <= explicitly warns that
|
||||
graphic.ex:4: Graphics.plot/1 # <= the :x key does not exist!
|
||||
```
|
||||
|
||||
Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples.
|
||||
|
||||
An alternative to refactor this anti-pattern is to use pattern matching, defining explicit clauses for 2d vs 3d points:
|
||||
|
||||
```elixir
|
||||
defmodule Graphics do
|
||||
# 3d
|
||||
def plot(%{x: x, y: y, z: z}) do
|
||||
# Some other code...
|
||||
{x, y, z}
|
||||
end
|
||||
|
||||
# 2d
|
||||
def plot(%{x: x, y: y}) do
|
||||
# Some other code...
|
||||
{x, y}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Pattern-matching is specially useful when matching over multiple keys as well as on the values themselves at once.
|
||||
|
||||
Another option is to use structs. By default, structs only support static access to its fields. In such scenarios, you may consider defining structs for both 2D and 3D points:
|
||||
|
||||
```elixir
|
||||
defmodule Point2D do
|
||||
@enforce_keys [:x, :y]
|
||||
defstruct [x: nil, y: nil]
|
||||
end
|
||||
```
|
||||
|
||||
Generally speaking, structs are useful when sharing data structures across modules, at the cost of adding a compile time dependency between these modules. If module `A` uses a struct defined in module `B`, `A` must be recompiled if the fields in the struct `B` change.
|
||||
|
||||
#### Additional remarks
|
||||
|
||||
This anti-pattern was formerly known as [Accessing non-existent map/struct fields](https://github.com/lucasvegi/Elixir-Code-Smells#accessing-non-existent-mapstruct-fields).
|
||||
|
||||
## Non-assertive pattern matching
|
||||
|
||||
#### Problem
|
||||
|
||||
Overall, Elixir systems are composed of many supervised processes, so the effects of an error are localized to a single process, and don't propagate to the entire application. A supervisor detects the failing process, reports it, and possibly restarts it. This anti-pattern arises when developers write defensive or imprecise code, capable of returning incorrect values which were not planned for, instead of programming in an assertive style through pattern matching and guards.
|
||||
|
||||
#### Example
|
||||
|
||||
The function `get_value/2` tries to extract a value from a specific key of a URL query string. As it is not implemented using pattern matching, `get_value/2` always returns a value, regardless of the format of the URL query string passed as a parameter in the call. Sometimes the returned value will be valid. However, if a URL query string with an unexpected format is used in the call, `get_value/2` will extract incorrect values from it:
|
||||
|
||||
```elixir
|
||||
defmodule Extract do
|
||||
def get_value(string, desired_key) do
|
||||
parts = String.split(string, "&")
|
||||
|
||||
Enum.find_value(parts, fn pair ->
|
||||
key_value = String.split(pair, "=")
|
||||
Enum.at(key_value, 0) == desired_key && Enum.at(key_value, 1)
|
||||
end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
# URL query string with the planned format - OK!
|
||||
iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "lab")
|
||||
"ASERG"
|
||||
iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "university")
|
||||
"UFMG"
|
||||
# Unplanned URL query string format - Unplanned value extraction!
|
||||
iex> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "university")
|
||||
"institution" # <= why not "institution=UFMG"? or only "UFMG"?
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will crash instead of returning an invalid value. This behaviour, shown below, allows clients to decide how to handle these errors and doesn't give a false impression that the code is working correctly when unexpected values are extracted:
|
||||
|
||||
```elixir
|
||||
defmodule Extract do
|
||||
def get_value(string, desired_key) do
|
||||
parts = String.split(string, "&")
|
||||
|
||||
Enum.find_value(parts, fn pair ->
|
||||
[key, value] = String.split(pair, "=") # <= pattern matching
|
||||
key == desired_key && value
|
||||
end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
# URL query string with the planned format - OK!
|
||||
iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "name")
|
||||
"Lucas"
|
||||
# Unplanned URL query string format - Crash explaining the problem to the client!
|
||||
iex> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "university")
|
||||
** (MatchError) no match of right hand side value: ["university", "institution", "UFMG"]
|
||||
extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair
|
||||
iex> Extract.get_value("name=Lucas&university&lab=ASERG", "university")
|
||||
** (MatchError) no match of right hand side value: ["university"]
|
||||
extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair
|
||||
```
|
||||
|
||||
Elixir and pattern matching promote an assertive style of programming where you handle the known cases. Once an unexpected scenario arises, you can decide to address it accordingly based on practical examples, or conclude the scenario is indeed invalid and the exception is the desired choice.
|
||||
|
||||
`case/2` is another important construct in Elixir that help us write assertive code, by matching on specific patterns. For example, if a function returns `{:ok, ...}` or `{:error, ...}`, prefer to explicitly match on both patterns:
|
||||
|
||||
```elixir
|
||||
case some_function(arg) do
|
||||
{:ok, value} -> # ...
|
||||
{:error, _} -> # ...
|
||||
end
|
||||
```
|
||||
|
||||
In particular, avoid matching solely on `_`, as shown below:
|
||||
|
||||
```elixir
|
||||
case some_function(arg) do
|
||||
{:ok, value} -> # ...
|
||||
_ -> # ...
|
||||
end
|
||||
```
|
||||
|
||||
Matching on `_` is less clear in intent and it may hide bugs if `some_function/1` adds new return values in the future.
|
||||
|
||||
#### Additional remarks
|
||||
|
||||
This anti-pattern was formerly known as [Speculative assumptions](https://github.com/lucasvegi/Elixir-Code-Smells#speculative-assumptions).
|
||||
|
||||
## Non-assertive truthiness
|
||||
|
||||
#### Problem
|
||||
|
||||
Elixir provides the concept of truthiness: `nil` and `false` are considered "falsy" and all other values are "truthy". Many constructs in the language, such as `&&/2`, `||/2`, and `!/1` handle truthy and falsy values. Using those operators is not an anti-pattern. However, using those operators when all operands are expected to be booleans, may be an anti-pattern.
|
||||
|
||||
#### Example
|
||||
|
||||
The simplest scenario where this anti-pattern manifests is in conditionals, such as:
|
||||
|
||||
```elixir
|
||||
if is_binary(name) && is_integer(age) do
|
||||
# ...
|
||||
else
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
Given both operands of `&&/2` are booleans, the code is more generic than necessary, and potentially unclear.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern, we can replace `&&/2`, `||/2`, and `!/1` by `and/2`, `or/2`, and `not/1` respectively. These operators assert at least their first argument is a boolean:
|
||||
|
||||
```elixir
|
||||
if is_binary(name) and is_integer(age) do
|
||||
# ...
|
||||
else
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
This technique may be particularly important when working with Erlang code. Erlang does not have the concept of truthiness. It never returns `nil`, instead its functions may return `:error` or `:undefined` in places an Elixir developer would return `nil`. Therefore, to avoid accidentally interpreting `:undefined` or `:error` as a truthy value, you may prefer to use `and/2`, `or/2`, and `not/1` exclusively when interfacing with Erlang APIs.
|
||||
@@ -0,0 +1,473 @@
|
||||
# Design-related anti-patterns
|
||||
|
||||
This document outlines potential anti-patterns related to your modules, functions, and the role they play within a codebase.
|
||||
|
||||
## Alternative return types
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern refers to functions that receive options (typically as a *keyword list* parameter) that drastically change their return type. Because options are optional and sometimes set dynamically, if they also change the return type, it may be hard to understand what the function actually returns.
|
||||
|
||||
#### Example
|
||||
|
||||
An example of this anti-pattern, as shown below, is when a function has many alternative return types, depending on the options received as a parameter.
|
||||
|
||||
```elixir
|
||||
defmodule AlternativeInteger do
|
||||
@spec parse(String.t(), keyword()) :: integer() | {integer(), String.t()} | :error
|
||||
def parse(string, options \\ []) when is_list(options) do
|
||||
if Keyword.get(options, :discard_rest, false) do
|
||||
Integer.parse(string)
|
||||
else
|
||||
case Integer.parse(string) do
|
||||
{int, _rest} -> int
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> AlternativeInteger.parse("13")
|
||||
13
|
||||
iex> AlternativeInteger.parse("13", discard_rest: true)
|
||||
13
|
||||
iex> AlternativeInteger.parse("13", discard_rest: false)
|
||||
{13, ""}
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To refactor this anti-pattern, as shown next, add a specific function for each return type (for example, `parse_discard_rest/1`), no longer delegating this to options passed as arguments.
|
||||
|
||||
```elixir
|
||||
defmodule AlternativeInteger do
|
||||
@spec parse(String.t()) :: {integer(), String.t()} | :error
|
||||
def parse(string) do
|
||||
Integer.parse(string)
|
||||
end
|
||||
|
||||
@spec parse_discard_rest(String.t()) :: integer() | :error
|
||||
def parse_discard_rest(string) do
|
||||
case Integer.parse(string) do
|
||||
{int, _rest} -> int
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> AlternativeInteger.parse("13")
|
||||
{13, ""}
|
||||
iex> AlternativeInteger.parse_discard_rest("13")
|
||||
13
|
||||
```
|
||||
|
||||
## Boolean obsession
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern happens when booleans are used instead of atoms to encode information. The usage of booleans themselves is not an anti-pattern, but whenever multiple booleans are used with overlapping states, replacing the booleans by atoms (or composite data types such as *tuples*) may lead to clearer code.
|
||||
|
||||
This is a special case of [*Primitive obsession*](#primitive-obsession), specific to boolean values.
|
||||
|
||||
#### Example
|
||||
|
||||
An example of this anti-pattern is a function that receives two or more options, such as `editor: true` and `admin: true`, to configure its behaviour in overlapping ways. In the code below, the `:editor` option has no effect if `:admin` is set, meaning that the `:admin` option has higher priority than `:editor`, and they are ultimately related.
|
||||
|
||||
```elixir
|
||||
defmodule MyApp do
|
||||
def process(invoice, options \\ []) do
|
||||
cond do
|
||||
options[:admin] -> # Is an admin
|
||||
options[:editor] -> # Is an editor
|
||||
true -> # Is none
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
Instead of using multiple options, the code above could be refactored to receive a single option, called `:role`, that can be either `:admin`, `:editor`, or `:default`:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp do
|
||||
def process(invoice, options \\ []) do
|
||||
case Keyword.get(options, :role, :default) do
|
||||
:admin -> # Is an admin
|
||||
:editor -> # Is an editor
|
||||
:default -> # Is none
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This anti-pattern may also happen in our own data structures. For example, we may define a `User` struct with two boolean fields, `:editor` and `:admin`, while a single field named `:role` may be preferred.
|
||||
|
||||
Finally, it is worth noting that using atoms may be preferred even when we have a single boolean argument/option. For example, consider an invoice which may be set as approved/unapproved. One option is to provide a function that expects a boolean:
|
||||
|
||||
```elixir
|
||||
MyApp.update(invoice, approved: true)
|
||||
```
|
||||
|
||||
However, using atoms may read better and make it simpler to add further states (such as pending) in the future:
|
||||
|
||||
```elixir
|
||||
MyApp.update(invoice, status: :approved)
|
||||
```
|
||||
|
||||
Remember booleans are internally represented as atoms. Therefore there is no performance penalty in one approach over the other.
|
||||
|
||||
## Exceptions for control-flow
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern refers to code that uses `Exception`s for control flow. Exception handling itself does not represent an anti-pattern, but developers must prefer to use `case` and pattern matching to change the flow of their code, instead of `try/rescue`. In turn, library authors should provide developers with APIs to handle errors without relying on exception handling. When developers have no freedom to decide if an error is exceptional or not, this is considered an anti-pattern.
|
||||
|
||||
#### Example
|
||||
|
||||
An example of this anti-pattern, as shown below, is using `try/rescue` to deal with file operations:
|
||||
|
||||
```elixir
|
||||
defmodule MyModule do
|
||||
def print_file(file) do
|
||||
try do
|
||||
IO.puts(File.read!(file))
|
||||
rescue
|
||||
e -> IO.puts(:stderr, Exception.message(e))
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> MyModule.print_file("valid_file")
|
||||
This is a valid file!
|
||||
:ok
|
||||
iex> MyModule.print_file("invalid_file")
|
||||
could not read file "invalid_file": no such file or directory
|
||||
:ok
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To refactor this anti-pattern, as shown next, use `File.read/1`, which returns tuples instead of raising when a file cannot be read:
|
||||
|
||||
```elixir
|
||||
defmodule MyModule do
|
||||
def print_file(file) do
|
||||
case File.read(file) do
|
||||
{:ok, binary} -> IO.puts(binary)
|
||||
{:error, reason} -> IO.puts(:stderr, "could not read file #{file}: #{reason}")
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This is only possible because the `File` module provides APIs for reading files with tuples as results (`File.read/1`), as well as a version that raises an exception (`File.read!/1`). The bang (exclamation point) is effectively part of [Elixir's naming conventions](naming-conventions.md#trailing-bang-foo).
|
||||
|
||||
Library authors are encouraged to follow the same practices. In practice, the bang variant is implemented on top of the non-raising version of the code. For example, `File.read!/1` is implemented as:
|
||||
|
||||
```elixir
|
||||
def read!(path) do
|
||||
case read(path) do
|
||||
{:ok, binary} ->
|
||||
binary
|
||||
|
||||
{:error, reason} ->
|
||||
raise File.Error, reason: reason, action: "read file", path: IO.chardata_to_string(path)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
A common practice followed by the community is to make the non-raising version return `{:ok, result}` or `{:error, Exception.t}`. For example, an HTTP client may return `{:ok, %HTTP.Response{}}` on success cases and `{:error, %HTTP.Error{}}` for failures, where `HTTP.Error` is [implemented as an exception](`Kernel.defexception/1`). This makes it convenient for anyone to raise an exception by simply calling `Kernel.raise/1`.
|
||||
|
||||
#### Additional remarks
|
||||
|
||||
This anti-pattern was formerly known as [Using exceptions for control-flow](https://github.com/lucasvegi/Elixir-Code-Smells#using-exceptions-for-control-flow).
|
||||
|
||||
## Primitive obsession
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are excessively used to carry structured information, rather than creating specific composite data types (for example, *tuples*, *maps*, and *structs*) that can better represent a domain.
|
||||
|
||||
#### Example
|
||||
|
||||
An example of this anti-pattern is the use of a single *string* to represent an `Address`. An `Address` is a more complex structure than a simple basic (aka, primitive) value.
|
||||
|
||||
```elixir
|
||||
defmodule MyApp do
|
||||
def extract_postal_code(address) when is_binary(address) do
|
||||
# Extract postal code with address...
|
||||
end
|
||||
|
||||
def fill_in_country(address) when is_binary(address) do
|
||||
# Fill in missing country...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
While you may receive the `address` as a string from a database, web request, or a third-party, if you find yourself frequently manipulating or extracting information from the string, it is a good indicator you should convert the address into structured data:
|
||||
|
||||
Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/).
|
||||
|
||||
#### Refactoring
|
||||
|
||||
Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we introduce a `parse/1` function, that converts the string into an `Address`, which will simplify the logic of remainng functions. With this modification, we can extract each field of this composite type individually when needed.
|
||||
|
||||
```elixir
|
||||
defmodule Address do
|
||||
defstruct [:street, :city, :state, :postal_code, :country]
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
defmodule MyApp do
|
||||
def parse(address) when is_binary(address) do
|
||||
# Returns %Address{}
|
||||
end
|
||||
|
||||
def extract_postal_code(%Address{} = address) do
|
||||
# Extract postal code with address...
|
||||
end
|
||||
|
||||
def fill_in_country(%Address{} = address) do
|
||||
# Fill in missing country...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Unrelated multi-clause function
|
||||
|
||||
#### Problem
|
||||
|
||||
Using multi-clause functions is a powerful Elixir feature. However, some developers may abuse this feature to group *unrelated* functionality, which is an anti-pattern.
|
||||
|
||||
#### Example
|
||||
|
||||
A frequent example of this usage of multi-clause functions occurs when developers mix unrelated business logic into the same function definition, in a way that the behaviour of each clause becomes completely distinct from the others. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them.
|
||||
|
||||
Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated.
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
Updates a struct.
|
||||
|
||||
If given a product, it will...
|
||||
|
||||
If given an animal, it will...
|
||||
"""
|
||||
def update(%Product{count: count, material: material}) do
|
||||
# ...
|
||||
end
|
||||
|
||||
def update(%Animal{count: count, skin: skin}) do
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
If updating an animal is completely different from updating a product and requires a different set of rules, it may be worth splitting those over different functions or even different modules.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
As shown below, a possible solution to this anti-pattern is to break the business rules that are mixed up in a single unrelated multi-clause function in simple functions. Each function can have a specific name and `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can impact the function's callers, so be careful!
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
Updates a product.
|
||||
|
||||
It will...
|
||||
"""
|
||||
def update_product(%Product{count: count, material: material}) do
|
||||
# ...
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates an animal.
|
||||
|
||||
It will...
|
||||
"""
|
||||
def update_animal(%Animal{count: count, skin: skin}) do
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
These functions may still be implemented with multiple clauses, as long as the clauses group related funtionality. For example, `update_product` could be in practice implemented as follows:
|
||||
|
||||
```elixir
|
||||
def update_product(%Product{count: 0}) do
|
||||
# ...
|
||||
end
|
||||
|
||||
def update_product(%Product{material: material})
|
||||
when material in ["metal", "glass"] do
|
||||
# ...
|
||||
end
|
||||
|
||||
def update_product(%Product{material: material})
|
||||
when material not in ["metal", "glass"] do
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
You can see this pattern in practice within Elixir itself. The `+/2` operator can add `Integer`s and `Float`s together, but not `String`s, which instead use the `<>/2` operator. In this sense, it is reasonable to handle integers and floats in the same operation, but strings are unrelated enough to deserve their own function.
|
||||
|
||||
You will also find examples in Elixir of functions that work with any struct, which would seemingly be an occurrence of this anti-pattern, such as `struct/2`:
|
||||
|
||||
```elixir
|
||||
iex> struct(URI.parse("/foo/bar"), path: "/bar/baz")
|
||||
%URI{
|
||||
scheme: nil,
|
||||
userinfo: nil,
|
||||
host: nil,
|
||||
port: nil,
|
||||
path: "/bar/baz",
|
||||
query: nil,
|
||||
fragment: nil
|
||||
}
|
||||
```
|
||||
|
||||
The difference here is that the `struct/2` function behaves precisely the same for any struct given, therefore there is no question of how the function handles different inputs. If the behaviour is clear and consistent for all inputs, then the anti-pattern does not take place.
|
||||
|
||||
## Using application configuration for libraries
|
||||
|
||||
#### Problem
|
||||
|
||||
The [*application environment*](https://hexdocs.pm/elixir/Application.html#module-the-application-environment) can be used to parameterize global values that can be used in an Elixir system. This mechanism can be very useful and therefore is not considered an anti-pattern by itself. However, library authors should avoid using the application environment to configure their library. The reason is exactly that the application environment is a **global** state, so there can only be a single value for each key in the environment for an application. This makes it impossible for multiple applications depending on the same library to configure the same aspect of the library in different ways.
|
||||
|
||||
#### Example
|
||||
|
||||
The `DashSplitter` module represents a library that configures the behavior of its functions through the global application environment. These configurations are concentrated in the *config/config.exs* file, shown below:
|
||||
|
||||
```elixir
|
||||
import Config
|
||||
|
||||
config :app_config,
|
||||
parts: 3
|
||||
|
||||
import_config "#{config_env()}.exs"
|
||||
```
|
||||
|
||||
One of the functions implemented by the `DashSplitter` library is `split/1`. This function aims to separate a string received via a parameter into a certain number of parts. The character used as a separator in `split/1` is always `"-"` and the number of parts the string is split into is defined globally by the application environment. This value is retrieved by the `split/1` function by calling `Application.fetch_env!/2`, as shown next:
|
||||
|
||||
```elixir
|
||||
defmodule DashSplitter do
|
||||
def split(string) when is_binary(string) do
|
||||
parts = Application.fetch_env!(:app_config, :parts) # <= retrieve parameterized value
|
||||
String.split(string, "-", parts: parts) # <= parts: 3
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Due to this parameterized value used by the `DashSplitter` library, all applications dependent on it can only use the `split/1` function with identical behavior about the number of parts generated by string separation. Currently, this value is equal to 3, as we can see in the use examples shown below:
|
||||
|
||||
```elixir
|
||||
iex> DashSplitter.split("Lucas-Francisco-Vegi")
|
||||
["Lucas", "Francisco", "Vegi"]
|
||||
iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi")
|
||||
["Lucas", "Francisco", "da-Matta-Vegi"]
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern, this type of configuration should be performed using a parameter passed to the function. The code shown below performs the refactoring of the `split/1` function by accepting [keyword lists](`Keyword`) as a new optional parameter. With this new parameter, it is possible to modify the default behavior of the function at the time of its call, allowing multiple different ways of using `split/2` within the same application:
|
||||
|
||||
```elixir
|
||||
defmodule DashSplitter do
|
||||
def split(string, opts \\ []) when is_binary(string) and is_list(opts) do
|
||||
parts = Keyword.get(opts, :parts, 2) # <= default config of parts == 2
|
||||
String.split(string, "-", parts: parts)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi", [parts: 5])
|
||||
["Lucas", "Francisco", "da", "Matta", "Vegi"]
|
||||
iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") #<= default config is used!
|
||||
["Lucas", "Francisco-da-Matta-Vegi"]
|
||||
```
|
||||
|
||||
Of course, not all uses of the application environment by libraries are incorrect. One example is using configuration to replace a component (or dependency) of a library by another that must behave the exact same. Consider a library that needs to parse CSV files. The library author may pick one package to use as default parser but allow its users to swap to different implementations via the application environment. At the end of the day, choosing a different CSV parser should not change the outcome, and library authors can even enforce this by [defining behaviours](../references/typespecs.md#behaviours) with the exact semantics they expect.
|
||||
|
||||
#### Additional remarks: Supervision trees
|
||||
|
||||
In practice, libraries may require additional configuration beyond keyword lists. For example, if a library needs to start a supervision tree, how can the user of said library customize its supervision tree? Given the supervision tree itself is global (as it belongs to the library), library authors may be tempted to use the application configuration once more.
|
||||
|
||||
One solution is for the library to provide its own child specification, instead of starting the supervision tree itself. This allows the user to start all necessary processes under its own supervision tree, potentially passing custom configuration options during initialization.
|
||||
|
||||
You can see this pattern in practice in projects like [Nx](https://github.com/elixir-nx/nx) and [DNS Cluster](https://github.com/phoenixframework/dns_cluster). These libraries require that you list processes under your own supervision tree:
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{DNSCluster, query: "my.subdomain"}
|
||||
]
|
||||
```
|
||||
|
||||
In such cases, if the users of `DNSCluster` need to configure DNSCluster per environment, they can be the ones reading from the application environment, without the library forcing them to:
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{DNSCluster, query: Application.get_env(:my_app, :dns_cluster_query) || :ignore}
|
||||
]
|
||||
```
|
||||
|
||||
Some libraries, such as [Ecto](https://github.com/elixir-ecto/ecto), allow you to pass your application name as an option (called `:otp_app` or similar) and then automatically read the environment from *your* application. While this addresses the issue with the application environment being global, as they read from each individual application, it comes at the cost of some indirection, compared to the example above where users explicitly read their application environment from their own code, whenever desired.
|
||||
|
||||
#### Additional remarks: Compile-time configuration
|
||||
|
||||
A similar discussion entails compile-time configuration. What if a library author requires some configuration to be provided at compilation time?
|
||||
|
||||
Once again, instead of forcing users of your library to provide compile-time configuration, you may want to allow users of your library to generate the code themselves. That's the approach taken by libraries such as [Ecto](https://github.com/elixir-ecto/ecto):
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Repo do
|
||||
use Ecto.Repo, adapter: Ecto.Adapters.Postgres
|
||||
end
|
||||
```
|
||||
|
||||
Instead of forcing developers to share a single repository, Ecto allows its users to define as many repositories as they want. Given the `:adapter` configuration is required at compile-time, it is a required value on `use Ecto.Repo`. If developers want to configure the adapter per environment, then it is their choice:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Repo do
|
||||
use Ecto.Repo, adapter: Application.compile_env(:my_app, :repo_adapter)
|
||||
end
|
||||
```
|
||||
|
||||
On the other hand, [code generation comes with its own anti-patterns](macro-anti-patterns.md), and must be considered carefully. That's to say: while using the application environment for libraries is discouraged, especially compile-time configuration, in some cases they may be the best option. For example, consider a library needs to parse CSV or JSON files to generate code based on data files. In such cases, it is best to provide reasonable defaults and make them customizable via the application environment, instead of asking each user of your library to generate the exact same code.
|
||||
|
||||
#### Additional remarks: Mix tasks
|
||||
|
||||
For Mix tasks and related tools, it may be necessary to provide per-project configuration. For example, imagine you have a `:linter` project, which supports setting the output file and the verbosity level. You may choose to configure it through application environment:
|
||||
|
||||
```elixir
|
||||
config :linter,
|
||||
output_file: "/path/to/output.json",
|
||||
verbosity: 3
|
||||
```
|
||||
|
||||
However, `Mix` allows tasks to read per-project configuration via `Mix.Project.config/0`. In this case, you can configure the `:linter` directly in the `mix.exs` file:
|
||||
|
||||
```elixir
|
||||
def project do
|
||||
[
|
||||
app: :my_app,
|
||||
version: "1.0.0",
|
||||
linter: [
|
||||
output_file: "/path/to/output.json",
|
||||
verbosity: 3
|
||||
],
|
||||
...
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Additionally, if a Mix task is available, you can also accept these options as command line arguments (see `OptionParser`):
|
||||
|
||||
```bash
|
||||
mix linter --output-file /path/to/output.json --verbosity 3
|
||||
```
|
||||
@@ -0,0 +1,213 @@
|
||||
# Meta-programming anti-patterns
|
||||
|
||||
This document outlines potential anti-patterns related to meta-programming.
|
||||
|
||||
## Large code generation
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern is related to macros that generate too much code. When a macro generates a large amount of code, it impacts how the compiler and/or the runtime work. The reason for this is that Elixir may have to expand, compile, and execute the code multiple times, which will make compilation slower and the resulting compiled artifacts larger.
|
||||
|
||||
#### Example
|
||||
|
||||
Imagine you are defining a router for a web application, where you could have macros like `get/2`. On every invocation of the macro (which could be hundreds), the code inside `get/2` will be expanded and compiled, which can generate a large volume of code overall.
|
||||
|
||||
```elixir
|
||||
defmodule Routes do
|
||||
defmacro get(route, handler) do
|
||||
quote do
|
||||
route = unquote(route)
|
||||
handler = unquote(handler)
|
||||
|
||||
if not is_binary(route) do
|
||||
raise ArgumentError, "route must be a binary"
|
||||
end
|
||||
|
||||
if not is_atom(handler) do
|
||||
raise ArgumentError, "handler must be a module"
|
||||
end
|
||||
|
||||
@store_route_for_compilation {route, handler}
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern, the developer should simplify the macro, delegating part of its work to other functions. As shown below, by encapsulating the code inside `quote/1` inside the function `__define__/3` instead, we reduce the code that is expanded and compiled on every invocation of the macro, and instead we dispatch to a function to do the bulk of the work.
|
||||
|
||||
```elixir
|
||||
defmodule Routes do
|
||||
defmacro get(route, handler) do
|
||||
quote do
|
||||
Routes.__define__(__MODULE__, unquote(route), unquote(handler))
|
||||
end
|
||||
end
|
||||
|
||||
def __define__(module, route, handler) do
|
||||
if not is_binary(route) do
|
||||
raise ArgumentError, "route must be a binary"
|
||||
end
|
||||
|
||||
if not is_atom(handler) do
|
||||
raise ArgumentError, "handler must be a module"
|
||||
end
|
||||
|
||||
Module.put_attribute(module, :store_route_for_compilation, {route, handler})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Unnecessary macros
|
||||
|
||||
#### Problem
|
||||
|
||||
*Macros* are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability.
|
||||
|
||||
#### Example
|
||||
|
||||
The `MyMath` module implements the `sum/2` macro to perform the sum of two numbers received as parameters. While this code has no syntax errors and can be executed correctly to get the desired result, it is unnecessarily more complex. By implementing this functionality as a macro rather than a conventional function, the code became less clear:
|
||||
|
||||
```elixir
|
||||
defmodule MyMath do
|
||||
defmacro sum(v1, v2) do
|
||||
quote do
|
||||
unquote(v1) + unquote(v2)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> require MyMath
|
||||
MyMath
|
||||
iex> MyMath.sum(3, 5)
|
||||
8
|
||||
iex> MyMath.sum(3 + 1, 5 + 6)
|
||||
15
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern, the developer must replace the unnecessary macro with structures that are simpler to write and understand, such as named functions. The code shown below is the result of the refactoring of the previous example. Basically, the `sum/2` macro has been transformed into a conventional named function. Note that the `require/2` call is no longer needed:
|
||||
|
||||
```elixir
|
||||
defmodule MyMath do
|
||||
def sum(v1, v2) do # <= The macro became a named function
|
||||
v1 + v2
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> MyMath.sum(3, 5)
|
||||
8
|
||||
iex> MyMath.sum(3+1, 5+6)
|
||||
15
|
||||
```
|
||||
|
||||
## `use` instead of `import`
|
||||
|
||||
#### Problem
|
||||
|
||||
Elixir has mechanisms such as `import/1`, `alias/1`, and `use/1` to establish dependencies between modules. Code implemented with these mechanisms does not characterize a smell by itself. However, while the `import/1` and `alias/1` directives have lexical scope and only facilitate a module calling functions of another, the `use/1` directive has a *broader scope*, which can be problematic.
|
||||
|
||||
The `use/1` directive allows a module to inject any type of code into another, including propagating dependencies. In this way, using the `use/1` directive makes code harder to read, because to understand exactly what will happen when it references a module, it is necessary to have knowledge of the internal details of the referenced module.
|
||||
|
||||
#### Example
|
||||
|
||||
The code shown below is an example of this anti-pattern. It defines three modules -- `ModuleA`, `Library`, and `ClientApp`. `ClientApp` is reusing code from the `Library` via the `use/1` directive, but is unaware of its internal details. This makes it harder for the author of `ClientApp` to visualize which modules and functionality are now available within its module. To make matters worse, `Library` also imports `ModuleA`, which defines a `foo/0` function that conflicts with a local function defined in `ClientApp`:
|
||||
|
||||
```elixir
|
||||
defmodule ModuleA do
|
||||
def foo do
|
||||
"From Module A"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
defmodule Library do
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import Library
|
||||
import ModuleA # <= propagating dependencies!
|
||||
end
|
||||
end
|
||||
|
||||
def from_lib do
|
||||
"From Library"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
defmodule ClientApp do
|
||||
use Library
|
||||
|
||||
def foo do
|
||||
"Local function from client app"
|
||||
end
|
||||
|
||||
def from_client_app do
|
||||
from_lib() <> " - " <> foo()
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
When we try to compile `ClientApp`, Elixir detects the conflict and throws the following error:
|
||||
|
||||
```text
|
||||
error: imported ModuleA.foo/0 conflicts with local function
|
||||
└ client_app.ex:4:
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To remove this anti-pattern, we recommend library authors avoid providing `__using__/1` callbacks whenever it can be replaced by `alias/1` or `import/1` directives. In the following code, we assume `use Library` is no longer available and `ClientApp` was refactored in this way, and with that, the code is clearer and the conflict as previously shown no longer exists:
|
||||
|
||||
```elixir
|
||||
defmodule ClientApp do
|
||||
import Library
|
||||
|
||||
def foo do
|
||||
"Local function from client app"
|
||||
end
|
||||
|
||||
def from_client_app do
|
||||
from_lib() <> " - " <> foo()
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> ClientApp.from_client_app()
|
||||
"From Library - Local function from client app"
|
||||
```
|
||||
|
||||
#### Additional remarks
|
||||
|
||||
In situations where you need to do more than importing and aliasing modules, providing `use MyModule` may be necessary, as it provides a common extension point within the Elixir ecosystem.
|
||||
|
||||
Therefore, to provide guidance and clarity, we recommend library authors to include an admonition block in their `@moduledoc` that explains how `use MyModule` impacts the developer's code. As an example, the `GenServer` documentation outlines:
|
||||
|
||||
> #### `use GenServer` {: .info}
|
||||
>
|
||||
> When you `use GenServer`, the `GenServer` module will
|
||||
> set `@behaviour GenServer` and define a `child_spec/1`
|
||||
> function, so your module can be used as a child
|
||||
> in a supervision tree.
|
||||
|
||||
Think of this summary as a ["Nutrition facts label"](https://en.wikipedia.org/wiki/Nutrition_facts_label) for code generation. Make sure to only list changes made to the public API of the module. For example, if `use Library` sets an internal attribute called `@_some_module_info` and this attribute is never meant to be public, avoid documenting it in the nutrition facts.
|
||||
|
||||
For convenience, the markup notation to generate the admonition block above is this:
|
||||
|
||||
```markdown
|
||||
> #### `use GenServer` {: .info}
|
||||
>
|
||||
> When you `use GenServer`, the `GenServer` module will
|
||||
> set `@behaviour GenServer` and define a `child_spec/1`
|
||||
> function, so your module can be used as a child
|
||||
> in a supervision tree.
|
||||
```
|
||||
@@ -0,0 +1,353 @@
|
||||
# Process-related anti-patterns
|
||||
|
||||
This document outlines potential anti-patterns related to processes and process-based abstractions.
|
||||
|
||||
## Code organization by process
|
||||
|
||||
#### Problem
|
||||
|
||||
This anti-pattern refers to code that is unnecessarily organized by processes. A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, error isolation, etc). When you use a process for code organization, it can create bottlenecks in the system.
|
||||
|
||||
#### Example
|
||||
|
||||
An example of this anti-pattern, as shown below, is a module that implements arithmetic operations (like `add` and `subtract`) by means of a `GenServer` process. If the number of calls to this single process grows, this code organization can compromise the system performance, therefore becoming a bottleneck.
|
||||
|
||||
```elixir
|
||||
defmodule Calculator do
|
||||
@moduledoc """
|
||||
Calculator that performs basic arithmetic operations.
|
||||
|
||||
This code is unnecessarily organized in a GenServer process.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
def add(a, b, pid) do
|
||||
GenServer.call(pid, {:add, a, b})
|
||||
end
|
||||
|
||||
def subtract(a, b, pid) do
|
||||
GenServer.call(pid, {:subtract, a, b})
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def init(init_arg) do
|
||||
{:ok, init_arg}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_call({:add, a, b}, _from, state) do
|
||||
{:reply, a + b, state}
|
||||
end
|
||||
|
||||
def handle_call({:subtract, a, b}, _from, state) do
|
||||
{:reply, a - b, state}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> {:ok, pid} = GenServer.start_link(Calculator, :init)
|
||||
{:ok, #PID<0.132.0>}
|
||||
iex> Calculator.add(1, 5, pid)
|
||||
6
|
||||
iex> Calculator.subtract(2, 3, pid)
|
||||
-1
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
In Elixir, as shown next, code organization must be done only through modules and functions. Whenever possible, a library should not impose specific behavior (such as parallelization) on its users. It is better to delegate this behavioral decision to the developers of clients, thus increasing the potential for code reuse of a library.
|
||||
|
||||
```elixir
|
||||
defmodule Calculator do
|
||||
def add(a, b) do
|
||||
a + b
|
||||
end
|
||||
|
||||
def subtract(a, b) do
|
||||
a - b
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> Calculator.add(1, 5)
|
||||
6
|
||||
iex> Calculator.subtract(2, 3)
|
||||
-1
|
||||
```
|
||||
|
||||
## Scattered process interfaces
|
||||
|
||||
#### Problem
|
||||
|
||||
In Elixir, the use of an `Agent`, a `GenServer`, or any other process abstraction is not an anti-pattern in itself. However, when the responsibility for direct interaction with a process is spread throughout the entire system, it can become problematic. This bad practice can increase the difficulty of code maintenance and make the code more prone to bugs.
|
||||
|
||||
#### Example
|
||||
|
||||
The following code seeks to illustrate this anti-pattern. The responsibility for interacting directly with the `Agent` is spread across four different modules (`A`, `B`, `C`, and `D`).
|
||||
|
||||
```elixir
|
||||
defmodule A do
|
||||
def update(process) do
|
||||
# Some other code...
|
||||
Agent.update(process, fn _list -> 123 end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
defmodule B do
|
||||
def update(process) do
|
||||
# Some other code...
|
||||
Agent.update(process, fn content -> %{a: content} end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
defmodule C do
|
||||
def update(process) do
|
||||
# Some other code...
|
||||
Agent.update(process, fn content -> [:atom_value | content] end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
defmodule D do
|
||||
def get(process) do
|
||||
# Some other code...
|
||||
Agent.get(process, fn content -> content end)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This spreading of responsibility can generate duplicated code and make code maintenance more difficult. Also, due to the lack of control over the format of the shared data, complex composed data can be shared. This freedom to use any format of data is dangerous and can induce developers to introduce bugs.
|
||||
|
||||
```elixir
|
||||
# start an agent with initial state of an empty list
|
||||
iex> {:ok, agent} = Agent.start_link(fn -> [] end)
|
||||
{:ok, #PID<0.135.0>}
|
||||
|
||||
# many data formats (for example, List, Map, Integer, Atom) are
|
||||
# combined through direct access spread across the entire system
|
||||
iex> A.update(agent)
|
||||
iex> B.update(agent)
|
||||
iex> C.update(agent)
|
||||
|
||||
# state of shared information
|
||||
iex> D.get(agent)
|
||||
[:atom_value, %{a: 123}]
|
||||
```
|
||||
|
||||
For a `GenServer` and other behaviours, this anti-pattern will manifest when scattering calls to `GenServer.call/3` and `GenServer.cast/2` throughout multiple modules, instead of encapsulating all the interaction with the `GenServer` in a single place.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
Instead of spreading direct access to a process abstraction, such as `Agent`, over many places in the code, it is better to refactor this code by centralizing the responsibility for interacting with a process in a single module. This refactoring improves maintainability by removing duplicated code; it also allows you to limit the accepted format for shared data, reducing bug-proneness. As shown below, the module `Foo.Bucket` is centralizing the responsibility for interacting with the `Agent`. Any other place in the code that needs to access shared data must now delegate this action to `Foo.Bucket`. Also, `Foo.Bucket` now only allows data to be shared in `Map` format.
|
||||
|
||||
```elixir
|
||||
defmodule Foo.Bucket do
|
||||
use Agent
|
||||
|
||||
def start_link(_opts) do
|
||||
Agent.start_link(fn -> %{} end)
|
||||
end
|
||||
|
||||
def get(bucket, key) do
|
||||
Agent.get(bucket, &Map.get(&1, key))
|
||||
end
|
||||
|
||||
def put(bucket, key, value) do
|
||||
Agent.update(bucket, &Map.put(&1, key, value))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The following are examples of how to delegate access to shared data (provided by an `Agent`) to `Foo.Bucket`.
|
||||
|
||||
```elixir
|
||||
# start an agent through `Foo.Bucket`
|
||||
iex> {:ok, bucket} = Foo.Bucket.start_link(%{})
|
||||
{:ok, #PID<0.114.0>}
|
||||
|
||||
# add shared values to the keys `milk` and `beer`
|
||||
iex> Foo.Bucket.put(bucket, "milk", 3)
|
||||
iex> Foo.Bucket.put(bucket, "beer", 7)
|
||||
|
||||
# access shared data of specific keys
|
||||
iex> Foo.Bucket.get(bucket, "beer")
|
||||
7
|
||||
iex> Foo.Bucket.get(bucket, "milk")
|
||||
3
|
||||
```
|
||||
|
||||
#### Additional remarks
|
||||
|
||||
This anti-pattern was formerly known as [Agent obsession](https://github.com/lucasvegi/Elixir-Code-Smells/tree/main#agent-obsession).
|
||||
|
||||
## Sending unnecessary data
|
||||
|
||||
#### Problem
|
||||
|
||||
Sending a message to a process can be an expensive operation if the message is big enough. That's because that message will be fully copied to the receiving process, which may be CPU and memory intensive. This is due to Erlang's "share nothing" architecture, where each process has its own memory, which simplifies and speeds up garbage collection.
|
||||
|
||||
This is more obvious when using `send/2`, `GenServer.call/3`, or the initial data in `GenServer.start_link/3`. Notably this also happens when using `spawn/1`, `Task.async/1`, `Task.async_stream/3`, and so on. It is more subtle here as the anonymous function passed to these functions captures the variables it references, and all captured variables will be copied over. By doing this, you can accidentally send way more data to a process than you actually need.
|
||||
|
||||
#### Example
|
||||
|
||||
Imagine you were to implement some simple reporting of IP addresses that made requests against your application. You want to do this asynchronously and not block processing, so you decide to use `spawn/1`. It may seem like a good idea to hand over the whole connection because we might need more data later. However passing the connection results in copying a lot of unnecessary data like the request body, params, etc.
|
||||
|
||||
```elixir
|
||||
# log_request_ip send the ip to some external service
|
||||
spawn(fn -> log_request_ip(conn) end)
|
||||
```
|
||||
|
||||
This problem also occurs when accessing only the relevant parts:
|
||||
|
||||
```elixir
|
||||
spawn(fn -> log_request_ip(conn.remote_ip) end)
|
||||
```
|
||||
|
||||
This will still copy over all of `conn`, because the `conn` variable is being captured inside the spawned function. The function then extracts the `remote_ip` field, but only after the whole `conn` has been copied over.
|
||||
|
||||
`send/2` and the `GenServer` APIs also rely on message passing. In the example below, the `conn` is once again copied to the underlying `GenServer`:
|
||||
|
||||
```elixir
|
||||
GenServer.cast(pid, {:report_ip_address, conn})
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
This anti-pattern has many potential remedies:
|
||||
|
||||
* Limit the data you send to the absolute necessary minimum instead of sending an entire struct. For example, don't send an entire `conn` struct if all you need is a couple of fields.
|
||||
|
||||
* If the only process that needs data is the one you are sending to, consider making the process fetch that data instead of passing it.
|
||||
|
||||
* Some abstractions, such as [`:persistent_term`](https://www.erlang.org/doc/man/persistent_term.html), allows you to share data between processes, as long as such data changes infrequently.
|
||||
|
||||
In our case, limiting the input data is a reasonable strategy. If all we need *right now* is the IP address, then let's only work with that and make sure we're only passing the IP address into the closure, like so:
|
||||
|
||||
```elixir
|
||||
ip_address = conn.remote_ip
|
||||
spawn(fn -> log_request_ip(ip_address) end)
|
||||
```
|
||||
|
||||
Or in the `GenServer` case:
|
||||
|
||||
```elixir
|
||||
GenServer.cast(pid, {:report_ip_address, conn.remote_ip})
|
||||
```
|
||||
|
||||
## Unsupervised processes
|
||||
|
||||
#### Problem
|
||||
|
||||
In Elixir, creating a process outside a supervision tree is not an anti-pattern in itself. However, when you spawn many long-running processes outside of supervision trees, this can make visibility and monitoring of these processes difficult, preventing developers from fully controlling their applications.
|
||||
|
||||
#### Example
|
||||
|
||||
The following code example seeks to illustrate a library responsible for maintaining a numerical `Counter` through a `GenServer` process *outside a supervision tree*. Multiple counters can be created simultaneously by a client (one process for each counter), making these *unsupervised* processes difficult to manage. This can cause problems with the initialization, restart, and shutdown of a system.
|
||||
|
||||
```elixir
|
||||
defmodule Counter do
|
||||
@moduledoc """
|
||||
Global counter implemented through a GenServer process.
|
||||
"""
|
||||
|
||||
use GenServer
|
||||
|
||||
@doc "Starts a counter process."
|
||||
def start_link(opts \\ []) do
|
||||
initial_value = Keyword.get(opts, :initial_value, 0)
|
||||
name = Keyword.get(opts, :name, __MODULE__)
|
||||
GenServer.start(__MODULE__, initial_value, name: name)
|
||||
end
|
||||
|
||||
@doc "Gets the current value of the given counter."
|
||||
def get(pid_name \\ __MODULE__) do
|
||||
GenServer.call(pid_name, :get)
|
||||
end
|
||||
|
||||
@doc "Bumps the value of the given counter."
|
||||
def bump(pid_name \\ __MODULE__, value) do
|
||||
GenServer.call(pid_name, {:bump, value})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(counter) do
|
||||
{:ok, counter}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get, _from, counter) do
|
||||
{:reply, counter, counter}
|
||||
end
|
||||
|
||||
def handle_call({:bump, value}, _from, counter) do
|
||||
{:reply, counter, counter + value}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> Counter.start_link()
|
||||
{:ok, #PID<0.115.0>}
|
||||
iex> Counter.get()
|
||||
0
|
||||
iex> Counter.start_link(initial_value: 15, name: :other_counter)
|
||||
{:ok, #PID<0.120.0>}
|
||||
iex> Counter.get(:other_counter)
|
||||
15
|
||||
iex> Counter.bump(:other_counter, -3)
|
||||
12
|
||||
iex> Counter.bump(Counter, 7)
|
||||
7
|
||||
```
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. One is initialized with `0`, the other with `15`. By means of this supervision tree, it is possible to manage the lifecycle of all child processes (stopping or restarting each one), improving the visibility of the entire app.
|
||||
|
||||
```elixir
|
||||
defmodule SupervisedProcess.Application do
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# With the default values for counter and name
|
||||
Counter,
|
||||
# With custom values for counter, name, and a custom ID
|
||||
Supervisor.child_spec(
|
||||
{Counter, name: :other_counter, initial_value: 15},
|
||||
id: :other_counter
|
||||
)
|
||||
]
|
||||
|
||||
Supervisor.start_link(children, strategy: :one_for_one, name: App.Supervisor)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> Supervisor.count_children(App.Supervisor)
|
||||
%{active: 2, specs: 2, supervisors: 0, workers: 2}
|
||||
iex> Counter.get(Counter)
|
||||
0
|
||||
iex> Counter.get(:other_counter)
|
||||
15
|
||||
iex> Counter.bump(Counter, 7)
|
||||
7
|
||||
iex> Supervisor.terminate_child(App.Supervisor, Counter)
|
||||
iex> Supervisor.count_children(App.Supervisor) # Only one active child
|
||||
%{active: 1, specs: 2, supervisors: 0, workers: 2}
|
||||
iex> Counter.get(Counter) # The process was terminated
|
||||
** (EXIT) no process: the process is not alive...
|
||||
iex> Supervisor.restart_child(App.Supervisor, Counter)
|
||||
iex> Counter.get(Counter) # After the restart, this process can be used again
|
||||
0
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
# What are anti-patterns?
|
||||
|
||||
Anti-patterns describe common mistakes or indicators of problems in code.
|
||||
They are also known as "code smells".
|
||||
|
||||
The goal of these guides is to document potential anti-patterns found in Elixir software
|
||||
and teach developers how to identify them and their pitfalls. If an existing piece
|
||||
of code matches an anti-pattern, it does not mean your code must be rewritten.
|
||||
Sometimes, even if a snippet matches a potential anti-pattern and its limitations,
|
||||
it may be the best approach to the problem at hand. No codebase is free of anti-patterns
|
||||
and one should not aim to remove all of them.
|
||||
|
||||
The anti-patterns in these guides are broken into 4 main categories:
|
||||
|
||||
* **Code-related anti-patterns:** related to your code and particular
|
||||
language idioms and features;
|
||||
|
||||
* **Design-related anti-patterns:** related to your modules, functions,
|
||||
and the role they play within a codebase;
|
||||
|
||||
* **Process-related anti-patterns:** related to processes and process-based
|
||||
abstractions;
|
||||
|
||||
* **Meta-programming anti-patterns:** related to meta-programming.
|
||||
|
||||
Each anti-pattern is documented using the following structure:
|
||||
|
||||
* **Name:** Unique identifier of the anti-pattern. This name is important to facilitate
|
||||
communication between developers;
|
||||
|
||||
* **Problem:** How the anti-pattern can harm code quality and what impacts this can have
|
||||
for developers;
|
||||
|
||||
* **Example:** Code and textual descriptions to illustrate the occurrence of the anti-pattern;
|
||||
|
||||
* **Refactoring:** Ways to change your code to improve its qualities. Examples of refactored
|
||||
code are presented to illustrate these changes.
|
||||
|
||||
An additional section with "Additional Remarks" may be provided. Those may include known scenarios where the anti-pattern does not apply.
|
||||
|
||||
The initial catalog of anti-patterns was proposed by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/). For more info, see [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf) and [the associated code repository](https://github.com/lucasvegi/Elixir-Code-Smells).
|
||||
|
||||
Additionally, the Security Working Group of the [Erlang Ecosystem Foundation](https://erlef.github.io/security-wg/) publishes [documents with security resources and best-practices of both Erlang and Elixir, including detailed guides for web applications](https://erlef.github.io/security-wg/).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
# alias, require, and import
|
||||
|
||||
In order to facilitate software reuse, Elixir provides three directives (`alias`, `require` and `import`) plus a macro called `use` summarized below:
|
||||
|
||||
```elixir
|
||||
# Alias the module so it can be called as Bar instead of Foo.Bar
|
||||
alias Foo.Bar, as: Bar
|
||||
|
||||
# Require the module in order to use its macros
|
||||
require Foo
|
||||
|
||||
# Import functions from Foo so they can be called without the `Foo.` prefix
|
||||
import Foo
|
||||
|
||||
# Invokes the custom code defined in Foo as an extension point
|
||||
use Foo
|
||||
```
|
||||
|
||||
We are going to explore them in detail now. Keep in mind the first three are called directives because they have *lexical scope*, while `use` is a common extension point that allows the used module to inject code.
|
||||
|
||||
## alias
|
||||
|
||||
`alias` allows you to set up aliases for any given module name.
|
||||
|
||||
Imagine a module uses a specialized list implemented in `Math.List`. The `alias` directive allows referring to `Math.List` just as `List` within the module definition:
|
||||
|
||||
```elixir
|
||||
defmodule Stats do
|
||||
alias Math.List, as: List
|
||||
# In the remaining module definition List expands to Math.List.
|
||||
end
|
||||
```
|
||||
|
||||
The original `List` can still be accessed within `Stats` by the fully-qualified name `Elixir.List`.
|
||||
|
||||
> All modules defined in Elixir are defined inside the main `Elixir` namespace, such as `Elixir.String`. However, for convenience, you can omit "Elixir." when referencing them.
|
||||
|
||||
Aliases are frequently used to define shortcuts. In fact, calling `alias` without an `:as` option sets the alias automatically to the last part of the module name, for example:
|
||||
|
||||
```elixir
|
||||
alias Math.List
|
||||
```
|
||||
|
||||
Is the same as:
|
||||
|
||||
```elixir
|
||||
alias Math.List, as: List
|
||||
```
|
||||
|
||||
Note that `alias` is *lexically scoped*, which allows you to set aliases inside specific functions:
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
def plus(a, b) do
|
||||
alias Math.List
|
||||
# ...
|
||||
end
|
||||
|
||||
def minus(a, b) do
|
||||
# ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In the example above, since we are invoking `alias` inside the function `plus/2`, the alias will be valid only inside the function `plus/2`. `minus/2` won't be affected at all.
|
||||
|
||||
## require
|
||||
|
||||
Elixir provides macros as a mechanism for meta-programming (writing code that generates code). Macros are expanded at compile time.
|
||||
|
||||
Public functions in modules are globally available, but in order to use macros, you need to opt-in by requiring the module they are defined in.
|
||||
|
||||
```elixir
|
||||
iex> Integer.is_odd(3)
|
||||
** (UndefinedFunctionError) function Integer.is_odd/1 is undefined or private. However, there is a macro with the same name and arity. Be sure to require Integer if you intend to invoke this macro
|
||||
(elixir) Integer.is_odd(3)
|
||||
iex> require Integer
|
||||
Integer
|
||||
iex> Integer.is_odd(3)
|
||||
true
|
||||
```
|
||||
|
||||
In Elixir, `Integer.is_odd/1` is defined as a macro so that it can be used as a guard. This means that, in order to invoke `Integer.is_odd/1`, we need to first require the `Integer` module.
|
||||
|
||||
Note that like the `alias` directive, `require` is also lexically scoped. We will talk more about macros in a later chapter.
|
||||
|
||||
## import
|
||||
|
||||
We use `import` whenever we want to access functions or macros from other modules without using the fully-qualified name. Note we can only import public functions, as private functions are never accessible externally.
|
||||
|
||||
For example, if we want to use the `duplicate/2` function from the `List` module several times, we can import it:
|
||||
|
||||
```elixir
|
||||
iex> import List, only: [duplicate: 2]
|
||||
List
|
||||
iex> duplicate(:ok, 3)
|
||||
[:ok, :ok, :ok]
|
||||
```
|
||||
|
||||
We imported only the function `duplicate` (with arity 2) from `List`. Although `:only` is optional, its usage is recommended in order to avoid importing all the functions of a given module inside the current scope. `:except` could also be given as an option in order to import everything in a module except a list of functions.
|
||||
|
||||
Note that `import` is *lexically scoped* too. This means that we can import specific macros or functions inside function definitions:
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
def some_function do
|
||||
import List, only: [duplicate: 2]
|
||||
duplicate(:ok, 10)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In the example above, the imported `List.duplicate/2` is only visible within that specific function. `duplicate/2` won't be available in any other function in that module (or any other module for that matter).
|
||||
|
||||
While `import`s can be a useful for frameworks and libraries to build abstractions, developers should generally prefer `alias` to `import` on their own codebases, as aliases make the origin of the function being invoked clearer.
|
||||
|
||||
## use
|
||||
|
||||
The `use` macro is frequently used as an extension point. This means that, when you `use` a module `FooBar`, you allow that module to inject *any* code in the current module, such as importing itself or other modules, defining new functions, setting a module state, etc.
|
||||
|
||||
For example, in order to write tests using the ExUnit framework, a developer should use the `ExUnit.Case` module:
|
||||
|
||||
```elixir
|
||||
defmodule AssertionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "always pass" do
|
||||
assert true
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Behind the scenes, `use` requires the given module and then calls the `__using__/1` callback on it allowing the module to inject some code into the current context. Some modules (for example, the above `ExUnit.Case`, but also `Supervisor` and `GenServer`) use this mechanism to populate your module with some basic behaviour, which your module is intended to override or complete.
|
||||
|
||||
Generally speaking, the following module:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
use Feature, option: :value
|
||||
end
|
||||
```
|
||||
|
||||
is compiled into
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
require Feature
|
||||
Feature.__using__(option: :value)
|
||||
end
|
||||
```
|
||||
|
||||
Since `use` allows any code to run, we can't really know the side-effects of using a module without reading its documentation. Therefore use this function with care and only if strictly required. Don't use `use` where an `import` or `alias` would do.
|
||||
|
||||
## Understanding Aliases
|
||||
|
||||
At this point, you may be wondering: what exactly is an Elixir alias and how is it represented?
|
||||
|
||||
An alias in Elixir is a capitalized identifier (like `String`, `Keyword`, etc) which is converted to an atom during compilation. For instance, the `String` alias translates by default to the atom `:"Elixir.String"`:
|
||||
|
||||
```elixir
|
||||
iex> is_atom(String)
|
||||
true
|
||||
iex> to_string(String)
|
||||
"Elixir.String"
|
||||
iex> :"Elixir.String" == String
|
||||
true
|
||||
```
|
||||
|
||||
By using the `alias/2` directive, we are changing the atom the alias expands to.
|
||||
|
||||
Aliases expand to atoms because in the Erlang Virtual Machine (and consequently Elixir) modules are always represented by atoms:
|
||||
|
||||
```elixir
|
||||
iex> List.flatten([1, [2], 3])
|
||||
[1, 2, 3]
|
||||
iex> :"Elixir.List".flatten([1, [2], 3])
|
||||
[1, 2, 3]
|
||||
```
|
||||
|
||||
That's the mechanism we use to call Erlang modules:
|
||||
|
||||
```elixir
|
||||
iex> :lists.flatten([1, [2], 3])
|
||||
[1, 2, 3]
|
||||
```
|
||||
|
||||
## Module nesting
|
||||
|
||||
Now that we have talked about aliases, we can talk about nesting and how it works in Elixir. Consider the following example:
|
||||
|
||||
```elixir
|
||||
defmodule Foo do
|
||||
defmodule Bar do
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The example above will define two modules: `Foo` and `Foo.Bar`. The second can be accessed as `Bar` inside `Foo` as long as they are in the same lexical scope.
|
||||
|
||||
If, later, the `Bar` module is moved outside the `Foo` module definition, it must be referenced by its full name (`Foo.Bar`) or an alias must be set using the `alias` directive discussed above.
|
||||
|
||||
**Note**: in Elixir, you don't have to define the `Foo` module before being able to define the `Foo.Bar` module, as they are effectively independent. The above could also be written as:
|
||||
|
||||
```elixir
|
||||
defmodule Foo.Bar do
|
||||
end
|
||||
|
||||
defmodule Foo do
|
||||
alias Foo.Bar
|
||||
# Can still access it as `Bar`
|
||||
end
|
||||
```
|
||||
|
||||
Aliasing a nested module does not bring parent modules into scope. Consider the following example:
|
||||
|
||||
```elixir
|
||||
defmodule Foo do
|
||||
defmodule Bar do
|
||||
defmodule Baz do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
alias Foo.Bar.Baz
|
||||
# The module `Foo.Bar.Baz` is now available as `Baz`
|
||||
# However, the module `Foo.Bar` is *not* available as `Bar`
|
||||
```
|
||||
|
||||
As we will see in later chapters, aliases also play a crucial role in macros, to guarantee they are hygienic.
|
||||
|
||||
## Multi alias/import/require/use
|
||||
|
||||
It is possible to `alias`, `import`, `require`, or `use` multiple modules at once. This is particularly useful once we start nesting modules, which is very common when building Elixir applications. For example, imagine you have an application where all modules are nested under `MyApp`, you can alias the modules `MyApp.Foo`, `MyApp.Bar` and `MyApp.Baz` at once as follows:
|
||||
|
||||
```elixir
|
||||
alias MyApp.{Foo, Bar, Baz}
|
||||
```
|
||||
|
||||
With this, we have finished our tour of Elixir modules. The next topic to cover is module attributes.
|
||||
@@ -0,0 +1,130 @@
|
||||
# Anonymous functions
|
||||
|
||||
Anonymous functions allow us to store and pass executable code around as if it was an integer or a string. Let's learn more.
|
||||
|
||||
## Defining anonymous functions
|
||||
|
||||
Anonymous functions in Elixir are delimited by the keywords `fn` and `end`:
|
||||
|
||||
```elixir
|
||||
iex> add = fn a, b -> a + b end
|
||||
#Function<12.71889879/2 in :erl_eval.expr/5>
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
iex> is_function(add)
|
||||
true
|
||||
```
|
||||
|
||||
In the example above, we defined an anonymous function that receives two arguments, `a` and `b`, and returns the result of `a + b`. The arguments are always on the left-hand side of `->` and the code to be executed on the right-hand side. The anonymous function is stored in the variable `add`.
|
||||
|
||||
We can invoke anonymous functions by passing arguments to it. Note that a dot (`.`) between the variable and parentheses is required to invoke an anonymous function. The dot makes it clear when you are calling an anonymous function, stored in the variable `add`, opposed to a function named `add/2`. For example, if you have an anonymous function stored in the variable `is_atom`, there is no ambiguity between `is_atom.(:foo)` and `is_atom(:foo)`. If both used the same `is_atom(:foo)` syntax, the only way to know the actual behaviour of `is_atom(:foo)` would be by scanning all code thus far for a possible definition of the `is_atom` variable. This scanning hurts maintainability as it requires developers to track additional context in their head when reading and writing code.
|
||||
|
||||
Anonymous functions in Elixir are also identified by the number of arguments they receive. We can check if a function is of any given arity by using `is_function/2`:
|
||||
|
||||
```elixir
|
||||
# check if add is a function that expects exactly 2 arguments
|
||||
iex> is_function(add, 2)
|
||||
true
|
||||
# check if add is a function that expects exactly 1 argument
|
||||
iex> is_function(add, 1)
|
||||
false
|
||||
```
|
||||
|
||||
## Closures
|
||||
|
||||
Anonymous functions can also access variables that are in scope when the function is defined. This is typically referred to as closures, as they close over their scope. Let's define a new anonymous function that uses the `add` anonymous function we have previously defined:
|
||||
|
||||
```elixir
|
||||
iex> double = fn a -> add.(a, a) end
|
||||
#Function<6.71889879/1 in :erl_eval.expr/5>
|
||||
iex> double.(2)
|
||||
4
|
||||
```
|
||||
|
||||
A variable assigned inside a function does not affect its surrounding environment:
|
||||
|
||||
```elixir
|
||||
iex> x = 42
|
||||
42
|
||||
iex> (fn -> x = 0 end).()
|
||||
0
|
||||
iex> x
|
||||
42
|
||||
```
|
||||
|
||||
## Clauses and guards
|
||||
|
||||
Similar to `case/2`, we can pattern match on the arguments of anonymous functions as well as define multiple clauses and guards:
|
||||
|
||||
```elixir
|
||||
iex> f = fn
|
||||
...> x, y when x > 0 -> x + y
|
||||
...> x, y -> x * y
|
||||
...> end
|
||||
#Function<12.71889879/2 in :erl_eval.expr/5>
|
||||
iex> f.(1, 3)
|
||||
4
|
||||
iex> f.(-1, 3)
|
||||
-3
|
||||
```
|
||||
|
||||
The number of arguments in each anonymous function clause needs to be the same, otherwise an error is raised.
|
||||
|
||||
```elixir
|
||||
iex> f2 = fn
|
||||
...> x, y when x > 0 -> x + y
|
||||
...> x, y, z -> x * y + z
|
||||
...> end
|
||||
** (CompileError) iex:1: cannot mix clauses with different arities in anonymous functions
|
||||
```
|
||||
|
||||
## The capture operator
|
||||
|
||||
Throughout this guide, we have been using the notation `name/arity` to refer to functions. It happens that this notation can actually be used to capture an existing function into a data-type we can pass around, similar to how anonymous functions behave.
|
||||
|
||||
```elixir
|
||||
iex> fun = &is_atom/1
|
||||
&:erlang.is_atom/1
|
||||
iex> is_function(fun)
|
||||
true
|
||||
iex> fun.(:hello)
|
||||
true
|
||||
iex> fun.(123)
|
||||
false
|
||||
```
|
||||
|
||||
As you can see, once a function is captured, we can pass it as argument or invoke it using the anonymous function notation. The returned value above also hints we can capture functions defined in modules:
|
||||
|
||||
```elixir
|
||||
iex> fun = &String.length/1
|
||||
&String.length/1
|
||||
iex> fun.("hello")
|
||||
5
|
||||
```
|
||||
|
||||
You can also capture operators:
|
||||
|
||||
```elixir
|
||||
iex> add = &+/2
|
||||
&:erlang.+/2
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
```
|
||||
|
||||
The capture syntax can also be used as a shortcut for creating functions. This is handy when you want to create functions that are mostly wrapping existing functions or operators:
|
||||
|
||||
```elixir
|
||||
iex> fun = &(&1 + 1)
|
||||
#Function<6.71889879/1 in :erl_eval.expr/5>
|
||||
iex> fun.(1)
|
||||
2
|
||||
|
||||
iex> fun2 = &"Good #{&1}"
|
||||
#Function<6.127694169/1 in :erl_eval.expr/5>
|
||||
iex> fun2.("morning")
|
||||
"Good morning"
|
||||
```
|
||||
|
||||
The `&1` represents the first argument passed into the function. `&(&1 + 1)` above is exactly the same as `fn x -> x + 1 end`. You can read more about the capture operator `&` in [its documentation](`&/1`).
|
||||
|
||||
Next let's revisit some of the data-types we learned in the past and dig deeper into how they work.
|
||||
@@ -0,0 +1,330 @@
|
||||
# Basic types
|
||||
|
||||
In this chapter we will learn more about Elixir basic types: integers, floats, booleans, atoms, and strings. Other data types, such as lists and tuples, will be explored in the next chapter.
|
||||
|
||||
```elixir
|
||||
iex> 1 # integer
|
||||
iex> 0x1F # integer
|
||||
iex> 1.0 # float
|
||||
iex> true # boolean
|
||||
iex> :atom # atom / symbol
|
||||
iex> "elixir" # string
|
||||
iex> [1, 2, 3] # list
|
||||
iex> {1, 2, 3} # tuple
|
||||
```
|
||||
|
||||
## Basic arithmetic
|
||||
|
||||
Open up `iex` and type the following expressions:
|
||||
|
||||
```elixir
|
||||
iex> 1 + 2
|
||||
3
|
||||
iex> 5 * 5
|
||||
25
|
||||
iex> 10 / 2
|
||||
5.0
|
||||
```
|
||||
|
||||
Notice that `10 / 2` returned a float `5.0` instead of an integer `5`. This is expected. In Elixir, the operator `/` always returns a float. If you want to do integer division or get the division remainder, you can invoke the `div` and `rem` functions:
|
||||
|
||||
```elixir
|
||||
iex> div(10, 2)
|
||||
5
|
||||
iex> div 10, 2
|
||||
5
|
||||
iex> rem 10, 3
|
||||
1
|
||||
```
|
||||
|
||||
Notice that Elixir allows you to drop the parentheses when invoking functions that expect one or more arguments. This feature gives a cleaner syntax when writing declarations and control-flow constructs. However, Elixir developers generally prefer to use parentheses.
|
||||
|
||||
Elixir also supports shortcut notations for entering binary, octal, and hexadecimal numbers:
|
||||
|
||||
```elixir
|
||||
iex> 0b1010
|
||||
10
|
||||
iex> 0o777
|
||||
511
|
||||
iex> 0x1F
|
||||
31
|
||||
```
|
||||
|
||||
Float numbers require a dot followed by at least one digit and also support `e` for scientific notation:
|
||||
|
||||
```elixir
|
||||
iex> 1.0
|
||||
1.0
|
||||
iex> 1.0e-10
|
||||
1.0e-10
|
||||
```
|
||||
|
||||
Floats in Elixir are 64-bit precision.
|
||||
|
||||
You can invoke the `round` function to get the closest integer to a given float, or the `trunc` function to get the integer part of a float.
|
||||
|
||||
```elixir
|
||||
iex> round(3.58)
|
||||
4
|
||||
iex> trunc(3.58)
|
||||
3
|
||||
```
|
||||
|
||||
Finally, we work with different data types, we will learn Elixir provides several predicate functions to check for the type of a value. For example, the `is_integer` can be used to check if a value is an integer or not:
|
||||
|
||||
```elixir
|
||||
iex> is_integer(1)
|
||||
true
|
||||
iex> is_integer(2.0)
|
||||
false
|
||||
```
|
||||
|
||||
You can also use `is_float` or `is_number` to check, respectively, if an argument is a float, or either an integer or float.
|
||||
|
||||
## Identifying functions and documentation
|
||||
|
||||
Before we move on to the next data type, let's talk about how Elixir identifies functions.
|
||||
|
||||
Functions in Elixir are identified by both their name and their arity. The arity of a function describes the number of arguments that the function takes. From this point on we will use both the function name and its arity to describe functions throughout the documentation. `trunc/1` identifies the function which is named `trunc` and takes `1` argument, whereas `trunc/2` identifies a different (nonexistent) function with the same name but with an arity of `2`.
|
||||
|
||||
We can also use this syntax to access documentation. The Elixir shell defines the `h` function, which you can use to access documentation for any function. For example, typing `h trunc/1` is going to print the documentation for the `trunc/1` function:
|
||||
|
||||
```elixir
|
||||
iex> h trunc/1
|
||||
def trunc()
|
||||
|
||||
Returns the integer part of number.
|
||||
```
|
||||
|
||||
`h trunc/1` works because it is defined in the `Kernel` module. All functions in the `Kernel` module are automatically imported into our namespace. Most often you will also include the module name when looking up for documentation for a given function:
|
||||
|
||||
```elixir
|
||||
iex> h Kernel.trunc/1
|
||||
def trunc()
|
||||
|
||||
Returns the integer part of number.
|
||||
```
|
||||
|
||||
You can use the module+function to lookup for anything, including operators (try `h Kernel.+/2`). Invoking `h` without arguments displays the documentation for `IEx.Helpers`, which is where `h` and other functionality is defined.
|
||||
|
||||
## Booleans and `nil`
|
||||
|
||||
Elixir supports `true` and `false` as booleans:
|
||||
|
||||
```elixir
|
||||
iex> true
|
||||
true
|
||||
iex> true == false
|
||||
false
|
||||
```
|
||||
|
||||
Elixir also provides three boolean operators: `or/2`, `and/2`, and `not/1`. These operators are strict in the sense that they expect something that evaluates to a boolean (`true` or `false`) as their first argument:
|
||||
|
||||
```elixir
|
||||
iex> true and true
|
||||
true
|
||||
iex> false or is_boolean(true)
|
||||
true
|
||||
```
|
||||
|
||||
Providing a non-boolean will raise an exception:
|
||||
|
||||
```elixir
|
||||
iex> 1 and true
|
||||
** (BadBooleanError) expected a boolean on left-side of "and", got: 1
|
||||
```
|
||||
|
||||
`or` and `and` are short-circuit operators. They only execute the right side if the left side is not enough to determine the result:
|
||||
|
||||
```elixir
|
||||
iex> false and raise("This error will never be raised")
|
||||
false
|
||||
iex> true or raise("This error will never be raised")
|
||||
true
|
||||
```
|
||||
|
||||
Elixir also provides the concept of `nil`, to indicate the absence of a value, and a set of logical operators that also manipulate `nil`: `||/2`, `&&/2`, and `!/1`. For these operators, `false` and `nil` are considered "falsy", all other values are considered "truthy":
|
||||
|
||||
```elixir
|
||||
# or
|
||||
iex> 1 || true
|
||||
1
|
||||
iex> false || 11
|
||||
11
|
||||
|
||||
# and
|
||||
iex> nil && 13
|
||||
nil
|
||||
iex> true && 17
|
||||
17
|
||||
|
||||
# not
|
||||
iex> !true
|
||||
false
|
||||
iex> !1
|
||||
false
|
||||
iex> !nil
|
||||
true
|
||||
```
|
||||
|
||||
## Atoms
|
||||
|
||||
An atom is a constant whose value is its own name. Some other languages call these symbols. They are often useful to enumerate over distinct values, such as:
|
||||
|
||||
```elixir
|
||||
iex> :apple
|
||||
:apple
|
||||
iex> :orange
|
||||
:orange
|
||||
iex> :watermelon
|
||||
:watermelon
|
||||
```
|
||||
|
||||
Atoms are equal if their names are equal.
|
||||
|
||||
```elixir
|
||||
iex> :apple == :apple
|
||||
true
|
||||
iex> :apple == :orange
|
||||
false
|
||||
```
|
||||
|
||||
Often they are used to express the state of an operation, by using values such as `:ok` and `:error`.
|
||||
|
||||
The booleans `true` and `false` are also atoms:
|
||||
|
||||
```elixir
|
||||
iex> true == :true
|
||||
true
|
||||
iex> is_atom(false)
|
||||
true
|
||||
iex> is_boolean(:false)
|
||||
true
|
||||
```
|
||||
|
||||
Elixir allows you to skip the leading `:` for the atoms `false`, `true` and `nil`.
|
||||
|
||||
## Strings
|
||||
|
||||
Strings in Elixir are delimited by double quotes, and they are encoded in UTF-8:
|
||||
|
||||
```elixir
|
||||
iex> "hellö"
|
||||
"hellö"
|
||||
```
|
||||
|
||||
> Note: if you are running on Windows, there is a chance your terminal does not use UTF-8 by default. You can change the encoding of your current session by running `chcp 65001` before entering IEx.
|
||||
|
||||
You can concatenate two strings with the `<>/2` operator:
|
||||
|
||||
```elixir
|
||||
iex> "hello " <> "world!"
|
||||
"hello world!"
|
||||
```
|
||||
|
||||
Elixir also supports string interpolation:
|
||||
|
||||
```elixir
|
||||
iex> string = "world"
|
||||
iex> "hello #{string}!"
|
||||
"hello world"
|
||||
```
|
||||
|
||||
String concatenation requires both sides to be strings but interpolation supports any data type that may be converted to a string:
|
||||
|
||||
```elixir
|
||||
iex> number = 42
|
||||
iex> "i am #{number} years old!"
|
||||
"i am 42 years old!"
|
||||
```
|
||||
|
||||
Strings can have line breaks in them. You can introduce them using escape sequences:
|
||||
|
||||
```elixir
|
||||
iex> "hello
|
||||
...> world"
|
||||
"hello\nworld"
|
||||
iex> "hello\nworld"
|
||||
"hello\nworld"
|
||||
```
|
||||
|
||||
You can print a string using the `IO.puts/1` function from the `IO` module:
|
||||
|
||||
```elixir
|
||||
iex> IO.puts("hello\nworld")
|
||||
hello
|
||||
world
|
||||
:ok
|
||||
```
|
||||
|
||||
Notice that the `IO.puts/1` function returns the atom `:ok` after printing.
|
||||
|
||||
Strings in Elixir are represented internally by contiguous sequences of bytes known as binaries:
|
||||
|
||||
```elixir
|
||||
iex> is_binary("hellö")
|
||||
true
|
||||
```
|
||||
|
||||
We can also get the number of bytes in a string:
|
||||
|
||||
```elixir
|
||||
iex> byte_size("hellö")
|
||||
6
|
||||
```
|
||||
|
||||
Notice that the number of bytes in that string is 6, even though it has 5 graphemes. That's because the grapheme "ö" takes 2 bytes to be represented in UTF-8. We can get the actual length of the string, based on the number of graphemes, by using the `String.length/1` function:
|
||||
|
||||
```elixir
|
||||
iex> String.length("hellö")
|
||||
5
|
||||
```
|
||||
|
||||
The `String` module contains a bunch of functions that operate on strings as defined in the Unicode standard:
|
||||
|
||||
```elixir
|
||||
iex> String.upcase("hellö")
|
||||
"HELLÖ"
|
||||
```
|
||||
|
||||
## Structural comparison
|
||||
|
||||
Elixir also provides `==`, `!=`, `<=`, `>=`, `<` and `>` as comparison operators. We can compare numbers:
|
||||
|
||||
```elixir
|
||||
iex> 1 == 1
|
||||
true
|
||||
iex> 1 != 2
|
||||
true
|
||||
iex> 1 < 2
|
||||
true
|
||||
```
|
||||
|
||||
But also atoms, strings, booleans, etc:
|
||||
|
||||
```elixir
|
||||
iex> "foo" == "foo"
|
||||
true
|
||||
iex> "foo" == "bar"
|
||||
false
|
||||
```
|
||||
|
||||
Integers and floats compare the same if they have the same value:
|
||||
|
||||
```elixir
|
||||
iex> 1 == 1.0
|
||||
true
|
||||
iex> 1 == 2.0
|
||||
false
|
||||
```
|
||||
|
||||
However, you can use the strict comparison operator `===` and `!==` if you want to distinguish between integers and floats (that's the only difference between these operators):
|
||||
|
||||
```elixir
|
||||
iex> 1 === 1.0
|
||||
false
|
||||
```
|
||||
|
||||
The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](`Kernel#module-structural-comparison`).
|
||||
|
||||
Elixir also provides data-types for expressing collections, such as lists and tuples, which we learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come on later chapters. Let's move forward.
|
||||
@@ -0,0 +1,300 @@
|
||||
# Binaries, strings, and charlists
|
||||
|
||||
In ["Basic types"](basic-types.md), we learned a bit about strings and we used the `is_binary/1` function for checks:
|
||||
|
||||
```elixir
|
||||
iex> string = "hello"
|
||||
"hello"
|
||||
iex> is_binary(string)
|
||||
true
|
||||
```
|
||||
|
||||
In this chapter, we will gain clarity on what exactly binaries are, how they relate to strings, and what single-quoted values, `'like this'`, mean in Elixir. Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, we have to educate ourselves about [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding.
|
||||
|
||||
## Unicode and Code Points
|
||||
|
||||
In order to facilitate meaningful communication between computers across multiple languages, a standard is required so that the ones and zeros on one machine mean the same thing when they are transmitted to another. The [Unicode Standard](https://unicode.org/standard/standard.html) acts as an official registry of virtually all the characters we know: this includes characters from classical and historical texts, emoji, and formatting and control characters as well.
|
||||
|
||||
Unicode organizes all of the characters in its repertoire into code charts, and each character is given a unique numerical index. This numerical index is known as a [Code Point](https://en.wikipedia.org/wiki/Code_point).
|
||||
|
||||
In Elixir you can use a `?` in front of a character literal to reveal its code point:
|
||||
|
||||
```elixir
|
||||
iex> ?a
|
||||
97
|
||||
iex> ?ł
|
||||
322
|
||||
```
|
||||
|
||||
Note that most Unicode code charts will refer to a code point by its hexadecimal (hex) representation, e.g. `97` translates to `0061` in hex, and we can represent any Unicode character in an Elixir string by using the `\uXXXX` notation and the hex representation of its code point number:
|
||||
|
||||
```elixir
|
||||
iex> "\u0061" == "a"
|
||||
true
|
||||
iex> 0x0061 = 97 = ?a
|
||||
97
|
||||
```
|
||||
|
||||
The hex representation will also help you look up information about a code point, e.g. [https://codepoints.net/U+0061](https://codepoints.net/U+0061) has a data sheet all about the lower case `a`, a.k.a. code point 97.
|
||||
|
||||
## UTF-8 and Encodings
|
||||
|
||||
Now that we understand what the Unicode standard is and what code points are, we can finally talk about encodings. Whereas the code point is **what** we store, an encoding deals with **how** we store it: encoding is an implementation. In other words, we need a mechanism to convert the code point numbers into bytes so they can be stored in memory, written to disk, etc.
|
||||
|
||||
Elixir uses UTF-8 to encode its strings, which means that code points are encoded as a series of 8-bit bytes. UTF-8 is a **variable width** character encoding that uses one to four bytes to store each code point. It is capable of encoding all valid Unicode code points. Let's see an example:
|
||||
|
||||
```elixir
|
||||
iex> string = "héllo"
|
||||
"héllo"
|
||||
iex> String.length(string)
|
||||
5
|
||||
iex> byte_size(string)
|
||||
6
|
||||
```
|
||||
|
||||
Although the string above has 5 characters, it uses 6 bytes, as two bytes are used to represent the character `é`.
|
||||
|
||||
> Note: if you are running on Windows, there is a chance your terminal does not use UTF-8 by default. You can change the encoding of your current session by running `chcp 65001` before entering `iex` (`iex.bat`).
|
||||
|
||||
Besides defining characters, UTF-8 also provides a notion of graphemes. Graphemes may consist of multiple characters that are often perceived as one. For example, the [woman firefighter emoji](https://emojipedia.org/woman-firefighter/) is represented as the combination of three characters: the woman emoji (👩), a hidden zero-width joiner, and the fire engine emoji (🚒):
|
||||
|
||||
```elixir
|
||||
iex> String.codepoints("👩🚒")
|
||||
["👩", "", "🚒"]
|
||||
iex> String.graphemes("👩🚒")
|
||||
["👩🚒"]
|
||||
```
|
||||
|
||||
However, Elixir is smart enough to know they are seen as a single character, and therefore the length is still one:
|
||||
|
||||
```elixir
|
||||
iex> String.length("👩🚒")
|
||||
1
|
||||
```
|
||||
|
||||
> Note: if you can't see the emoji above in your terminal, you need to make sure your terminal supports emoji and that you are using a font that can render them.
|
||||
|
||||
Although these rules may sound complicated, UTF-8 encoded documents are everywhere. This page itself is encoded in UTF-8. The encoding information is given to your browser which then knows how to render all of the bytes, characters, and graphemes accordingly.
|
||||
|
||||
If you want to see the exact bytes that a string would be stored in a file, a common trick is to concatenate the null byte `<<0>>` to it:
|
||||
|
||||
```elixir
|
||||
iex> "hełło" <> <<0>>
|
||||
<<104, 101, 197, 130, 197, 130, 111, 0>>
|
||||
```
|
||||
|
||||
Alternatively, you can view a string's binary representation by using `IO.inspect/2`:
|
||||
|
||||
```elixir
|
||||
iex> IO.inspect("hełło", binaries: :as_binaries)
|
||||
<<104, 101, 197, 130, 197, 130, 111>>
|
||||
```
|
||||
|
||||
We are getting a little bit ahead of ourselves. Let's talk about bitstrings to learn about what exactly the `<<>>` constructor means.
|
||||
|
||||
## Bitstrings
|
||||
|
||||
Although we have covered code points and UTF-8 encoding, we still need to go a bit deeper into how exactly we store the encoded bytes, and this is where we introduce the **bitstring**. A bitstring is a fundamental data type in Elixir, denoted with the `<<>>/1` syntax. **A bitstring is a contiguous sequence of bits in memory.**
|
||||
|
||||
By default, 8 bits (i.e. 1 byte) is used to store each number in a bitstring, but you can manually specify the number of bits via a `::n` modifier to denote the size in `n` bits, or you can use the more verbose declaration `::size(n)`:
|
||||
|
||||
```elixir
|
||||
iex> <<42>> == <<42::8>>
|
||||
true
|
||||
iex> <<3::4>>
|
||||
<<3::size(4)>>
|
||||
```
|
||||
|
||||
For example, the decimal number `3` when represented with 4 bits in base 2 would be `0011`, which is equivalent to the values `0`, `0`, `1`, `1`, each stored using 1 bit:
|
||||
|
||||
```elixir
|
||||
iex> <<0::1, 0::1, 1::1, 1::1>> == <<3::4>>
|
||||
true
|
||||
```
|
||||
|
||||
Any value that exceeds what can be stored by the number of bits provisioned is truncated:
|
||||
|
||||
```elixir
|
||||
iex> <<1>> == <<257>>
|
||||
true
|
||||
```
|
||||
|
||||
Here, 257 in base 2 would be represented as `100000001`, but since we have reserved only 8 bits for its representation (by default), the left-most bit is ignored and the value becomes truncated to `00000001`, or simply `1` in decimal.
|
||||
|
||||
## Binaries
|
||||
|
||||
**A binary is a bitstring where the number of bits is divisible by 8.** That means that every binary is a bitstring, but not every bitstring is a binary. We can use the `is_bitstring/1` and `is_binary/1` functions to demonstrate this.
|
||||
|
||||
```elixir
|
||||
iex> is_bitstring(<<3::4>>)
|
||||
true
|
||||
iex> is_binary(<<3::4>>)
|
||||
false
|
||||
iex> is_bitstring(<<0, 255, 42>>)
|
||||
true
|
||||
iex> is_binary(<<0, 255, 42>>)
|
||||
true
|
||||
iex> is_binary(<<42::16>>)
|
||||
true
|
||||
```
|
||||
|
||||
We can pattern match on binaries / bitstrings:
|
||||
|
||||
```elixir
|
||||
iex> <<0, 1, x>> = <<0, 1, 2>>
|
||||
<<0, 1, 2>>
|
||||
iex> x
|
||||
2
|
||||
iex> <<0, 1, x>> = <<0, 1, 2, 3>>
|
||||
** (MatchError) no match of right hand side value: <<0, 1, 2, 3>>
|
||||
```
|
||||
|
||||
Note that unless you explicitly use `::` modifiers, each entry in the binary pattern is expected to match a single byte (exactly 8 bits). If we want to match on a binary of unknown size, we can use the `binary` modifier at the end of the pattern:
|
||||
|
||||
```elixir
|
||||
iex> <<0, 1, x::binary>> = <<0, 1, 2, 3>>
|
||||
<<0, 1, 2, 3>>
|
||||
iex> x
|
||||
<<2, 3>>
|
||||
```
|
||||
|
||||
There are a couple other modifiers that can be useful when doing pattern matches on binaries. The `binary-size(n)` modifier will match `n` bytes in a binary:
|
||||
|
||||
```elixir
|
||||
iex> <<head::binary-size(2), rest::binary>> = <<0, 1, 2, 3>>
|
||||
<<0, 1, 2, 3>>
|
||||
iex> head
|
||||
<<0, 1>>
|
||||
iex> rest
|
||||
<<2, 3>>
|
||||
```
|
||||
|
||||
**A string is a UTF-8 encoded binary**, where the code point for each character is encoded using 1 to 4 bytes. Thus every string is a binary, but due to the UTF-8 standard encoding rules, not every binary is a valid string.
|
||||
|
||||
```elixir
|
||||
iex> is_binary("hello")
|
||||
true
|
||||
iex> is_binary(<<239, 191, 19>>)
|
||||
true
|
||||
iex> String.valid?(<<239, 191, 19>>)
|
||||
false
|
||||
```
|
||||
|
||||
The string concatenation operator `<>` is actually a binary concatenation operator:
|
||||
|
||||
```elixir
|
||||
iex> "a" <> "ha"
|
||||
"aha"
|
||||
iex> <<0, 1>> <> <<2, 3>>
|
||||
<<0, 1, 2, 3>>
|
||||
```
|
||||
|
||||
Given that strings are binaries, we can also pattern match on strings:
|
||||
|
||||
```elixir
|
||||
iex> <<head, rest::binary>> = "banana"
|
||||
"banana"
|
||||
iex> head == ?b
|
||||
true
|
||||
iex> rest
|
||||
"anana"
|
||||
```
|
||||
|
||||
However, remember that binary pattern matching works on *bytes*, so matching on the string like "über" with multibyte characters won't match on the *character*, it will match on the *first byte of that character*:
|
||||
|
||||
```elixir
|
||||
iex> "ü" <> <<0>>
|
||||
<<195, 188, 0>>
|
||||
iex> <<x, rest::binary>> = "über"
|
||||
"über"
|
||||
iex> x == ?ü
|
||||
false
|
||||
iex> rest
|
||||
<<188, 98, 101, 114>>
|
||||
```
|
||||
|
||||
Above, `x` matched on only the first byte of the multibyte `ü` character.
|
||||
|
||||
Therefore, when pattern matching on strings, it is important to use the `utf8` modifier:
|
||||
|
||||
```elixir
|
||||
iex> <<x::utf8, rest::binary>> = "über"
|
||||
"über"
|
||||
iex> x == ?ü
|
||||
true
|
||||
iex> rest
|
||||
"ber"
|
||||
```
|
||||
|
||||
## Charlists
|
||||
|
||||
Our tour of our bitstrings, binaries, and strings is nearly complete, but we have one more data type to explain: the charlist.
|
||||
|
||||
**A charlist is a list of integers where all the integers are valid code points.** In practice, you will not come across them often, only in specific scenarios such as interfacing with older Erlang libraries that do not accept binaries as arguments.
|
||||
|
||||
```elixir
|
||||
iex> ~c"hello"
|
||||
~c"hello"
|
||||
iex> [?h, ?e, ?l, ?l, ?o]
|
||||
~c"hello"
|
||||
```
|
||||
|
||||
The `~c` sigil (we'll cover sigils later in the ["Sigils"](sigils.md) chapter) indicates the fact that we are dealing with a charlist and not a regular string.
|
||||
|
||||
Instead of containing bytes, a charlist contains integer code points. However, the list is only printed as a sigil if all code points are within the ASCII range:
|
||||
|
||||
```elixir
|
||||
iex> ~c"hełło"
|
||||
[104, 101, 322, 322, 111]
|
||||
iex> is_list(~c"hełło")
|
||||
true
|
||||
```
|
||||
|
||||
This is done to ease interoperability with Erlang, even though it may lead to some surprising behaviour. For example, if you are storing a list of integers that happen to range between 0 and 127, by default IEx will interpret this as a charlist and it will display the corresponding ASCII characters.
|
||||
|
||||
```elixir
|
||||
iex> heartbeats_per_minute = [99, 97, 116]
|
||||
~c"cat"
|
||||
```
|
||||
|
||||
You can always force charlists to be printed in their list representation by calling the `inspect/2` function:
|
||||
|
||||
```elixir
|
||||
iex> inspect(heartbeats_per_minute, charlists: :as_list)
|
||||
"[99, 97, 116]"
|
||||
```
|
||||
|
||||
Furthermore, you can convert a charlist to a string and back by using the `to_string/1` and `to_charlist/1`:
|
||||
|
||||
```elixir
|
||||
iex> to_charlist("hełło")
|
||||
[104, 101, 322, 322, 111]
|
||||
iex> to_string(~c"hełło")
|
||||
"hełło"
|
||||
iex> to_string(:hello)
|
||||
"hello"
|
||||
iex> to_string(1)
|
||||
"1"
|
||||
```
|
||||
|
||||
The functions above are polymorphic, in other words, they accept many shapes: not only do they convert charlists to strings (and vice-versa), they can also convert integers, atoms, and so on.
|
||||
|
||||
String (binary) concatenation uses the `<>` operator but charlists, being lists, use the list concatenation operator `++`:
|
||||
|
||||
```elixir
|
||||
iex> ~c"this " <> ~c"fails"
|
||||
** (ArgumentError) expected binary argument in <> operator but got: ~c"this "
|
||||
(elixir) lib/kernel.ex:1821: Kernel.wrap_concatenation/3
|
||||
(elixir) lib/kernel.ex:1808: Kernel.extract_concatenations/2
|
||||
(elixir) expanding macro: Kernel.<>/2
|
||||
iex:1: (file)
|
||||
iex> ~c"this " ++ ~c"works"
|
||||
~c"this works"
|
||||
iex> "he" ++ "llo"
|
||||
** (ArgumentError) argument error
|
||||
:erlang.++("he", "llo")
|
||||
iex> "he" <> "llo"
|
||||
"hello"
|
||||
```
|
||||
|
||||
With binaries, strings, and charlists out of the way, it is time to talk about key-value data structures.
|
||||
@@ -0,0 +1,171 @@
|
||||
# case, cond, and if
|
||||
|
||||
In this chapter, we will learn about the `case`, `cond`, and `if` control flow structures.
|
||||
|
||||
## case
|
||||
|
||||
`case` allows us to compare a value against many patterns until we find a matching one:
|
||||
|
||||
```elixir
|
||||
iex> case {1, 2, 3} do
|
||||
...> {4, 5, 6} ->
|
||||
...> "This clause won't match"
|
||||
...> {1, x, 3} ->
|
||||
...> "This clause will match and bind x to 2 in this clause"
|
||||
...> _ ->
|
||||
...> "This clause would match any value"
|
||||
...> end
|
||||
"This clause will match and bind x to 2 in this clause"
|
||||
```
|
||||
|
||||
If you want to pattern match against an existing variable, you need to use the `^` operator:
|
||||
|
||||
```elixir
|
||||
iex> x = 1
|
||||
1
|
||||
iex> case 10 do
|
||||
...> ^x -> "Won't match"
|
||||
...> _ -> "Will match"
|
||||
...> end
|
||||
"Will match"
|
||||
```
|
||||
|
||||
Clauses also allow extra conditions to be specified via guards:
|
||||
|
||||
```elixir
|
||||
iex> case {1, 2, 3} do
|
||||
...> {1, x, 3} when x > 0 ->
|
||||
...> "Will match"
|
||||
...> _ ->
|
||||
...> "Would match, if guard condition were not satisfied"
|
||||
...> end
|
||||
"Will match"
|
||||
```
|
||||
|
||||
The first clause above will only match when `x` is positive.
|
||||
|
||||
Keep in mind errors in guards do not leak but simply make the guard fail:
|
||||
|
||||
```elixir
|
||||
iex> hd(1)
|
||||
** (ArgumentError) argument error
|
||||
iex> case 1 do
|
||||
...> x when hd(x) -> "Won't match"
|
||||
...> x -> "Got #{x}"
|
||||
...> end
|
||||
"Got 1"
|
||||
```
|
||||
|
||||
If none of the clauses match, an error is raised:
|
||||
|
||||
```elixir
|
||||
iex> case :ok do
|
||||
...> :error -> "Won't match"
|
||||
...> end
|
||||
** (CaseClauseError) no case clause matching: :ok
|
||||
```
|
||||
|
||||
The documentation for the `Kernel` module lists all available guards in its sidebar. You can also consult the complete [Patterns and Guards](../references/patterns-and-guards.md#guards) reference for in-depth documentation.
|
||||
|
||||
## cond
|
||||
|
||||
`case` is useful when you need to match against different values. However, in many circumstances, we want to check different conditions and find the first one that does not evaluate to `nil` or `false`. In such cases, one may use `cond`:
|
||||
|
||||
```elixir
|
||||
iex> cond do
|
||||
...> 2 + 2 == 5 ->
|
||||
...> "This will not be true"
|
||||
...> 2 * 2 == 3 ->
|
||||
...> "Nor this"
|
||||
...> 1 + 1 == 2 ->
|
||||
...> "But this will"
|
||||
...> end
|
||||
"But this will"
|
||||
```
|
||||
|
||||
This is equivalent to `else if` clauses in many imperative languages - although used less frequently in Elixir.
|
||||
|
||||
If all of the conditions return `nil` or `false`, an error (`CondClauseError`) is raised. For this reason, it may be necessary to add a final condition, equal to `true`, which will always match:
|
||||
|
||||
```elixir
|
||||
iex> cond do
|
||||
...> 2 + 2 == 5 ->
|
||||
...> "This is never true"
|
||||
...> 2 * 2 == 3 ->
|
||||
...> "Nor this"
|
||||
...> true ->
|
||||
...> "This is always true (equivalent to else)"
|
||||
...> end
|
||||
"This is always true (equivalent to else)"
|
||||
```
|
||||
|
||||
Finally, note `cond` considers any value besides `nil` and `false` to be true:
|
||||
|
||||
```elixir
|
||||
iex> cond do
|
||||
...> hd([1, 2, 3]) ->
|
||||
...> "1 is considered as true"
|
||||
...> end
|
||||
"1 is considered as true"
|
||||
```
|
||||
|
||||
## if/unless
|
||||
|
||||
Besides `case` and `cond`, Elixir also provides `if/2` and `unless/2`, which are useful when you need to check for only one condition:
|
||||
|
||||
```elixir
|
||||
iex> if true do
|
||||
...> "This works!"
|
||||
...> end
|
||||
"This works!"
|
||||
iex> unless true do
|
||||
...> "This will never be seen"
|
||||
...> end
|
||||
nil
|
||||
```
|
||||
|
||||
If the condition given to `if/2` returns `false` or `nil`, the body given between `do`-`end` is not executed and instead it returns `nil`. The opposite happens with `unless/2`.
|
||||
|
||||
They also support `else` blocks:
|
||||
|
||||
```elixir
|
||||
iex> if nil do
|
||||
...> "This won't be seen"
|
||||
...> else
|
||||
...> "This will"
|
||||
...> end
|
||||
"This will"
|
||||
```
|
||||
|
||||
This is also a good opportunity to talk about variable scoping in Elixir. If any variable is declared or changed inside `if`, `case`, and similar constructs, the declaration and change will only be visible inside the construct. For example:
|
||||
|
||||
```elixir
|
||||
iex> x = 1
|
||||
1
|
||||
iex> if true do
|
||||
...> x = x + 1
|
||||
...> end
|
||||
2
|
||||
iex> x
|
||||
1
|
||||
```
|
||||
|
||||
In said cases, if you want to change a value, you must return the value from the `if`:
|
||||
|
||||
```elixir
|
||||
iex> x = 1
|
||||
1
|
||||
iex> x = if true do
|
||||
...> x + 1
|
||||
...> else
|
||||
...> x
|
||||
...> end
|
||||
2
|
||||
```
|
||||
|
||||
> #### `if` and `unless` are macros {: .info}
|
||||
>
|
||||
> An interesting note regarding `if/2` and `unless/2` is that they are implemented as macros in the language: they aren't special language constructs as they would be in many languages. You can check the documentation and their source for more information.
|
||||
|
||||
We have concluded the introduction to the most fundamental control-flow constructs in Elixir. Now
|
||||
let's learn where code and data meet with anonymous functions.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Comprehensions
|
||||
|
||||
In Elixir, it is common to loop over an Enumerable, often filtering out some results and mapping values into another list. Comprehensions are syntactic sugar for such constructs: they group those common tasks into the `for` special form.
|
||||
|
||||
For example, we can map a list of integers into their squared values:
|
||||
|
||||
```elixir
|
||||
iex> for n <- [1, 2, 3, 4], do: n * n
|
||||
[1, 4, 9, 16]
|
||||
```
|
||||
|
||||
A comprehension is made of three parts: generators, filters, and collectables.
|
||||
|
||||
## Generators and filters
|
||||
|
||||
In the expression above, `n <- [1, 2, 3, 4]` is the **generator**. It is literally generating values to be used in the comprehension. Any enumerable can be passed on the right-hand side of the generator expression:
|
||||
|
||||
```elixir
|
||||
iex> for n <- 1..4, do: n * n
|
||||
[1, 4, 9, 16]
|
||||
```
|
||||
|
||||
Generator expressions also support pattern matching on their left-hand side; all non-matching patterns are *ignored*. Imagine that, instead of a range, we have a keyword list where the key is the atom `:good` or `:bad` and we only want to compute the square of the `:good` values:
|
||||
|
||||
```elixir
|
||||
iex> values = [good: 1, good: 2, bad: 3, good: 4]
|
||||
iex> for {:good, n} <- values, do: n * n
|
||||
[1, 4, 16]
|
||||
```
|
||||
|
||||
Alternatively to pattern matching, filters can be used to select some particular elements. For example, we can select the multiples of 3 and discard all others:
|
||||
|
||||
```elixir
|
||||
iex> for n <- 0..5, rem(n, 3) == 0, do: n * n
|
||||
[0, 9]
|
||||
```
|
||||
|
||||
Comprehensions discard all elements for which the filter expression returns `false` or `nil`; all other values are selected.
|
||||
|
||||
Comprehensions generally provide a much more concise representation than using the equivalent functions from the `Enum` and `Stream` modules. Furthermore, comprehensions also allow multiple generators and filters to be given. Here is an example that receives a list of directories and gets the size of each file in those directories:
|
||||
|
||||
```elixir
|
||||
dirs = ["/home/mikey", "/home/james"]
|
||||
|
||||
for dir <- dirs,
|
||||
file <- File.ls!(dir),
|
||||
path = Path.join(dir, file),
|
||||
File.regular?(path) do
|
||||
File.stat!(path).size
|
||||
end
|
||||
```
|
||||
|
||||
Multiple generators can also be used to calculate the cartesian product of two lists:
|
||||
|
||||
```elixir
|
||||
iex> for i <- [:a, :b, :c], j <- [1, 2], do: {i, j}
|
||||
[a: 1, a: 2, b: 1, b: 2, c: 1, c: 2]
|
||||
```
|
||||
|
||||
Finally, keep in mind that variable assignments inside the comprehension, be it in generators, filters or inside the block, are not reflected outside of the comprehension.
|
||||
|
||||
## Bitstring generators
|
||||
|
||||
Bitstring generators are also supported and are very useful when you need to comprehend over bitstring streams. The example below receives a list of pixels from a binary with their respective red, green and blue values and converts them into tuples of three elements each:
|
||||
|
||||
```elixir
|
||||
iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>>
|
||||
iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
|
||||
[{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
|
||||
```
|
||||
|
||||
A bitstring generator can be mixed with "regular" enumerable generators, and supports filters as well.
|
||||
|
||||
## The `:into` option
|
||||
|
||||
In the examples above, all the comprehensions returned lists as their result. However, the result of a comprehension can be inserted into different data structures by passing the `:into` option to the comprehension.
|
||||
|
||||
For example, a bitstring generator can be used with the `:into` option in order to easily remove all spaces in a string:
|
||||
|
||||
```elixir
|
||||
iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>
|
||||
"helloworld"
|
||||
```
|
||||
|
||||
Sets, maps, and other dictionaries can also be given to the `:into` option. In general, `:into` accepts any structure that implements the `Collectable` protocol.
|
||||
|
||||
A common use case of `:into` can be transforming values in a map:
|
||||
|
||||
```elixir
|
||||
iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val}
|
||||
%{"a" => 1, "b" => 4}
|
||||
```
|
||||
|
||||
Let's make another example using streams. Since the `IO` module provides streams (that are both `Enumerable`s and `Collectable`s), an echo terminal that echoes back the upcased version of whatever is typed can be implemented using comprehensions:
|
||||
|
||||
```elixir
|
||||
iex> stream = IO.stream(:stdio, :line)
|
||||
iex> for line <- stream, into: stream do
|
||||
...> String.upcase(line) <> "\n"
|
||||
...> end
|
||||
```
|
||||
|
||||
Now type any string into the terminal and you will see that the same value will be printed in upper-case. Unfortunately, this example also got your IEx shell stuck in the comprehension, so you will need to hit `Ctrl+C` twice to get out of it. :)
|
||||
|
||||
## Other options
|
||||
|
||||
Comprehensions support other options, such as `:reduce` and `:uniq`. Here are additional resources to learn more about comprehensions:
|
||||
|
||||
* [`for` official reference in Elixir documentation](`for/1`)
|
||||
* [Mitchell Hanberg's comprehensive guide to Elixir's comprehensions](https://www.mitchellhanberg.com/the-comprehensive-guide-to-elixirs-for-comprehension/)
|
||||
@@ -0,0 +1,169 @@
|
||||
# Debugging
|
||||
|
||||
There are a number of ways to debug code in Elixir. In this chapter we will cover some of the more common ways of doing so.
|
||||
|
||||
## IO.inspect/2
|
||||
|
||||
What makes `IO.inspect(item, opts \\ [])` really useful in debugging is that it returns the `item` argument passed to it without affecting the behavior of the original code. Let's see an example.
|
||||
|
||||
```elixir
|
||||
(1..10)
|
||||
|> IO.inspect()
|
||||
|> Enum.map(fn x -> x * 2 end)
|
||||
|> IO.inspect()
|
||||
|> Enum.sum()
|
||||
|> IO.inspect()
|
||||
```
|
||||
|
||||
Prints:
|
||||
|
||||
```elixir
|
||||
1..10
|
||||
[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
|
||||
110
|
||||
```
|
||||
|
||||
As you can see `IO.inspect/2` makes it possible to "spy" on values almost anywhere in your code without altering the result, making it very helpful inside of a pipeline like in the above case.
|
||||
|
||||
`IO.inspect/2` also provides the ability to decorate the output with a `label` option. The label will be printed before the inspected `item`:
|
||||
|
||||
```elixir
|
||||
[1, 2, 3]
|
||||
|> IO.inspect(label: "before")
|
||||
|> Enum.map(&(&1 * 2))
|
||||
|> IO.inspect(label: "after")
|
||||
|> Enum.sum
|
||||
```
|
||||
|
||||
Prints:
|
||||
|
||||
```elixir
|
||||
before: [1, 2, 3]
|
||||
after: [2, 4, 6]
|
||||
```
|
||||
|
||||
It is also very common to use `IO.inspect/2` with `binding/0`, which returns all variable names and their values:
|
||||
|
||||
```elixir
|
||||
def some_fun(a, b, c) do
|
||||
IO.inspect binding()
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
When `some_fun/3` is invoked with `:foo`, `"bar"`, `:baz` it prints:
|
||||
|
||||
```elixir
|
||||
[a: :foo, b: "bar", c: :baz]
|
||||
```
|
||||
|
||||
See `IO.inspect/2` and `Inspect.Opts` respectively to learn more about the function and read about all supported options.
|
||||
|
||||
## dbg/2
|
||||
|
||||
Elixir v1.14 introduced `dbg/2`. `dbg` is similar to `IO.inspect/2` but specifically tailored for debugging. It prints the value passed to it and returns it (just like `IO.inspect/2`), but it also prints the code and location.
|
||||
|
||||
```elixir
|
||||
# In my_file.exs
|
||||
feature = %{name: :dbg, inspiration: "Rust"}
|
||||
dbg(feature)
|
||||
dbg(Map.put(feature, :in_version, "1.14.0"))
|
||||
```
|
||||
|
||||
The code above prints this:
|
||||
|
||||
```shell
|
||||
[my_file.exs:2: (file)]
|
||||
feature #=> %{inspiration: "Rust", name: :dbg}
|
||||
[my_file.exs:3: (file)]
|
||||
Map.put(feature, :in_version, "1.14.0") #=> %{in_version: "1.14.0", inspiration: "Rust", name: :dbg}
|
||||
```
|
||||
|
||||
When talking about `IO.inspect/2`, we mentioned its usefulness when placed between steps of `|>` pipelines. `dbg` does it better: it understands Elixir code, so it will print values at *every step of the pipeline*.
|
||||
|
||||
```elixir
|
||||
# In dbg_pipes.exs
|
||||
__ENV__.file
|
||||
|> String.split("/", trim: true)
|
||||
|> List.last()
|
||||
|> File.exists?()
|
||||
|> dbg()
|
||||
```
|
||||
|
||||
This code prints:
|
||||
|
||||
```shell
|
||||
[dbg_pipes.exs:5: (file)]
|
||||
__ENV__.file #=> "/home/myuser/dbg_pipes.exs"
|
||||
|> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"]
|
||||
|> List.last() #=> "dbg_pipes.exs"
|
||||
|> File.exists?() #=> true
|
||||
```
|
||||
|
||||
While `dbg` provides conveniences around Elixir constructs, you will need `IEx` if you want to execute code and set breakpoints while debugging.
|
||||
|
||||
## Breakpoints
|
||||
|
||||
When using `IEx`, you may pass `--dbg pry` as an option to "stop" the code execution where the `dbg` call is:
|
||||
|
||||
```console
|
||||
$ iex --dbg pry
|
||||
```
|
||||
|
||||
Now a call to `dbg` will ask if you want to pry the existing code. If you accept, you'll be able to access all variables, as well as imports and aliases from the code, directly from IEx. This is called "prying". While the pry session is running, the code execution stops, until `continue` or `next` are called. Remember you can always run `iex` in the context of a project with `iex -S mix TASK`.
|
||||
|
||||
<script id="asciicast-509509" src="https://asciinema.org/a/509509.js" async></script>
|
||||
|
||||
`dbg` calls require us to change the code we intend to debug and has limited stepping functionality. Luckily IEx also provides a `IEx.break!/2` function which allows you to set and manage breakpoints on any Elixir code without modifying its source:
|
||||
|
||||
<script type="text/javascript" src="https://asciinema.org/a/0h3po0AmTcBAorc5GBNU97nrs.js" id="asciicast-0h3po0AmTcBAorc5GBNU97nrs" async></script><noscript><p><a href="https://asciinema.org/a/0h3po0AmTcBAorc5GBNU97nrs">See the example in asciinema</a></p></noscript>
|
||||
|
||||
Similar to `dbg`, once a breakpoint is reached code execution stops until `continue` or `next` are invoked. However, `break!/2` does not have access to aliases and imports from the debugged code as it works on the compiled artifact rather than on source code.
|
||||
|
||||
## Observer
|
||||
|
||||
For debugging complex systems, jumping at the code is not enough. It is necessary to have an understanding of the whole virtual machine, processes, applications, as well as set up tracing mechanisms. Luckily this can be achieved in Erlang with `:observer`. In your application:
|
||||
|
||||
```elixir
|
||||
$ iex
|
||||
iex> :observer.start()
|
||||
```
|
||||
|
||||
> #### Missing dependencies {: .warning}
|
||||
>
|
||||
> When running `iex` inside a project with `iex -S mix`, `observer` won't be available as a dependency. To do so, you will need to call the following functions before:
|
||||
>
|
||||
> ```elixir
|
||||
> iex> Mix.ensure_application!(:wx)
|
||||
> iex> Mix.ensure_application!(:runtime_tools)
|
||||
> iex> Mix.ensure_application!(:observer)
|
||||
> iex> :observer.start()
|
||||
> ```
|
||||
>
|
||||
> If any of the calls above fail, here is what may have happened: some package managers default to installing a minimized Erlang without WX bindings for GUI support. In some package managers, you may be able to replace the headless Erlang with a more complete package (look for packages named `erlang` vs `erlang-nox` on Debian/Ubuntu/Arch). In others managers, you may need to install a separate `erlang-wx` (or similarly named) package.
|
||||
>
|
||||
> There are conversations to improve this experience in future releases.
|
||||
|
||||
The above will open another Graphical User Interface that provides many panes to fully understand and navigate the runtime and your project.
|
||||
|
||||
We explore the Observer in the context of an actual project [in the Dynamic Supervisor chapter of the Mix & OTP guide](../mix-and-otp/dynamic-supervisor.md). This is one of the debugging techniques [the Phoenix framework used to achieve 2 million connections on a single machine](https://phoenixframework.org/blog/the-road-to-2-million-websocket-connections).
|
||||
|
||||
If you are using the Phoenix web framework, it ships with the [Phoenix LiveDashboard](https://github.com/phoenixframework/phoenix_live_dashboard), a web dashboard for production nodes which provides similar features to Observer.
|
||||
|
||||
Finally, remember you can also get a mini-overview of the runtime info by calling `runtime_info/0` directly in IEx.
|
||||
|
||||
## Other tools and community
|
||||
|
||||
We have just scratched the surface of what the Erlang VM has to offer, for example:
|
||||
|
||||
* Alongside the observer application, Erlang also includes a [`:crashdump_viewer`](https://www.erlang.org/doc/man/crashdump_viewer.html) to view crash dumps
|
||||
|
||||
* Integration with OS level tracers, such as [Linux Trace Toolkit,](http://www.erlang.org/doc/apps/runtime_tools/LTTng.html) [DTRACE,](http://www.erlang.org/doc/apps/runtime_tools/DTRACE.html) and [SystemTap](http://www.erlang.org/doc/apps/runtime_tools/SYSTEMTAP.html)
|
||||
|
||||
* [Microstate accounting](http://www.erlang.org/doc/man/msacc.html) measures how much time the runtime spends in several low-level tasks in a short time interval
|
||||
|
||||
* Mix ships with many tasks under the `profile` namespace, such as `cprof` and `fprof`
|
||||
|
||||
* For more advanced use cases, we recommend the excellent [Erlang in Anger](https://www.erlang-in-anger.com/), which is available as a free ebook
|
||||
|
||||
Happy debugging!
|
||||
@@ -0,0 +1,124 @@
|
||||
# Enumerables and Streams
|
||||
|
||||
While Elixir allows us to write recursive code, most operations we perform on collections is done with the help of the `Enum` and `Stream` modules. Let's learn how.
|
||||
|
||||
## Enumerables
|
||||
|
||||
Elixir provides the concept of enumerables and the `Enum` module to work with them. We have already learned two enumerables: lists and maps.
|
||||
|
||||
```elixir
|
||||
iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
|
||||
[2, 4, 6]
|
||||
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
|
||||
[2, 12]
|
||||
```
|
||||
|
||||
The `Enum` module provides a huge range of functions to transform, sort, group, filter and retrieve items from enumerables. It is one of the modules developers use frequently in their Elixir code. For a general overview of all functions in the `Enum` module, see [the `Enum` cheatsheet](enum-cheat.cheatmd).
|
||||
|
||||
Elixir also provides ranges (see `Range`), which are also enumerable:
|
||||
|
||||
```elixir
|
||||
iex> Enum.map(1..3, fn x -> x * 2 end)
|
||||
[2, 4, 6]
|
||||
iex> Enum.reduce(1..3, 0, &+/2)
|
||||
6
|
||||
```
|
||||
|
||||
The functions in the `Enum` module are limited to, as the name says, enumerating values in data structures. For specific operations, like inserting and updating particular elements, you may need to reach for modules specific to the data type. For example, if you want to insert an element at a given position in a list, you should use the `List.insert_at/3` function, as it would make little sense to insert a value into, for example, a range.
|
||||
|
||||
We say the functions in the `Enum` module are polymorphic because they can work with diverse data types. In particular, the functions in the `Enum` module can work with any data type that implements the `Enumerable` protocol. We are going to discuss Protocols in a later chapter, for now we are going to move on to a specific kind of enumerable called a stream.
|
||||
|
||||
## Eager vs Lazy
|
||||
|
||||
All the functions in the `Enum` module are eager. Many functions expect an enumerable and return a list back:
|
||||
|
||||
```elixir
|
||||
iex> odd? = fn x -> rem(x, 2) != 0 end
|
||||
#Function<6.80484245/1 in :erl_eval.expr/5>
|
||||
iex> Enum.filter(1..3, odd?)
|
||||
[1, 3]
|
||||
```
|
||||
|
||||
This means that when performing multiple operations with `Enum`, each operation is going to generate an intermediate list until we reach the result:
|
||||
|
||||
```elixir
|
||||
iex> 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum()
|
||||
7500000000
|
||||
```
|
||||
|
||||
The example above has a pipeline of operations. We start with a range and then multiply each element in the range by 3. This first operation will now create and return a list with `100_000` items. Then we keep all odd elements from the list, generating a new list, now with `50_000` items, and then we sum all entries.
|
||||
|
||||
## The pipe operator
|
||||
|
||||
The `|>` symbol used in the snippet above is the **pipe operator**: it takes the output from the expression on its left side and passes it as the first argument to the function call on its right side. Its purpose is to highlight the data being transformed by a series of functions. To see how it can make the code cleaner, have a look at the example above rewritten without using the `|>` operator:
|
||||
|
||||
```elixir
|
||||
iex> Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
|
||||
7500000000
|
||||
```
|
||||
|
||||
Find more about the pipe operator [by reading its documentation](`|>/2`).
|
||||
|
||||
## Streams
|
||||
|
||||
As an alternative to `Enum`, Elixir provides the `Stream` module which supports lazy operations:
|
||||
|
||||
```elixir
|
||||
iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum()
|
||||
7500000000
|
||||
```
|
||||
|
||||
Streams are lazy, composable enumerables.
|
||||
|
||||
In the example above, `1..100_000 |> Stream.map(&(&1 * 3))` returns a data type, an actual stream, that represents the `map` computation over the range `1..100_000`:
|
||||
|
||||
```elixir
|
||||
iex> 1..100_000 |> Stream.map(&(&1 * 3))
|
||||
#Stream<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>
|
||||
```
|
||||
|
||||
Furthermore, they are composable because we can pipe many stream operations:
|
||||
|
||||
```elixir
|
||||
iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
|
||||
#Stream<[enum: 1..100000, funs: [...]]>
|
||||
```
|
||||
|
||||
Instead of generating intermediate lists, streams build a series of computations that are invoked only when we pass the underlying stream to the `Enum` module. Streams are useful when working with large, *possibly infinite*, collections.
|
||||
|
||||
Many functions in the `Stream` module accept any enumerable as an argument and return a stream as a result. It also provides functions for creating streams. For example, `Stream.cycle/1` can be used to create a stream that cycles a given enumerable infinitely. Be careful to not call a function like `Enum.map/2` on such streams, as they would cycle forever:
|
||||
|
||||
```elixir
|
||||
iex> stream = Stream.cycle([1, 2, 3])
|
||||
#Function<15.16982430/2 in Stream.unfold/2>
|
||||
iex> Enum.take(stream, 10)
|
||||
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
|
||||
```
|
||||
|
||||
On the other hand, `Stream.unfold/2` can be used to generate values from a given initial value:
|
||||
|
||||
```elixir
|
||||
iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
|
||||
#Function<39.75994740/2 in Stream.unfold/2>
|
||||
iex> Enum.take(stream, 3)
|
||||
["h", "e", "ł"]
|
||||
```
|
||||
|
||||
Another interesting function is `Stream.resource/3` which can be used to wrap around resources, guaranteeing they are opened right before enumeration and closed afterwards, even in the case of failures. For example, `File.stream!/1` builds on top of `Stream.resource/3` to stream files:
|
||||
|
||||
```elixir
|
||||
iex> stream = File.stream!("path/to/file")
|
||||
%File.Stream{
|
||||
line_or_bytes: :line,
|
||||
modes: [:raw, :read_ahead, :binary],
|
||||
path: "path/to/file",
|
||||
raw: true
|
||||
}
|
||||
iex> Enum.take(stream, 10)
|
||||
```
|
||||
|
||||
The example above will fetch the first 10 lines of the file you have selected. This means streams can be very useful for handling large files or even slow resources like network resources.
|
||||
|
||||
The `Enum` and `Stream` modules provide a wide range of functions, but you don't have to know all of them by heart. Familiarize yourself with `Enum.map/2`, `Enum.reduce/3` and other functions with either `map` or `reduce` in their names, and you will naturally build an intuition around the most important use cases. You may also focus on the `Enum` module first and only move to `Stream` for the particular scenarios where laziness is required, to either deal with slow resources or large, possibly infinite, collections.
|
||||
|
||||
Next, we'll look at a feature central to Elixir, Processes, which allows us to write concurrent, parallel and distributed programs in an easy and understandable way.
|
||||
@@ -0,0 +1,188 @@
|
||||
# Erlang libraries
|
||||
|
||||
Elixir provides excellent interoperability with Erlang libraries. In fact, Elixir discourages simply wrapping Erlang libraries in favor of directly interfacing with Erlang code. In this section, we will present some of the most common and useful Erlang functionality that is not found in Elixir.
|
||||
|
||||
Erlang modules have a different naming convention than in Elixir and start in lowercase. In both cases, module names are atoms and we invoke functions by dispatching to the module name:
|
||||
|
||||
```elixir
|
||||
iex> is_atom(String)
|
||||
true
|
||||
iex> String.first("hello")
|
||||
"h"
|
||||
iex> is_atom(:binary)
|
||||
true
|
||||
iex> :binary.first("hello")
|
||||
104
|
||||
```
|
||||
|
||||
As you grow more proficient in Elixir, you may want to explore the Erlang [STDLIB Reference Manual](http://www.erlang.org/doc/apps/stdlib/index.html) in more detail.
|
||||
|
||||
## The binary module
|
||||
|
||||
The built-in Elixir String module handles binaries that are UTF-8 encoded. [The `:binary` module](`:binary`) is useful when you are dealing with binary data that is not necessarily UTF-8 encoded.
|
||||
|
||||
```elixir
|
||||
iex> String.to_charlist("Ø")
|
||||
[216]
|
||||
iex> :binary.bin_to_list("Ø")
|
||||
[195, 152]
|
||||
```
|
||||
|
||||
The above example shows the difference; the `String` module returns Unicode codepoints, while `:binary` deals with raw data bytes.
|
||||
|
||||
## Formatted text output
|
||||
|
||||
Elixir does not contain a function similar to `printf` found in C and other languages. Luckily, the Erlang standard library functions `:io.format/2` and `:io_lib.format/2` may be used. The first formats to terminal output, while the second formats to an iolist. The format specifiers differ from `printf`, [refer to the Erlang documentation for details](`:io.format/2`).
|
||||
|
||||
```elixir
|
||||
iex> :io.format("Pi is approximately given by:~10.3f~n", [:math.pi])
|
||||
Pi is approximately given by: 3.142
|
||||
:ok
|
||||
iex> to_string(:io_lib.format("Pi is approximately given by:~10.3f~n", [:math.pi]))
|
||||
"Pi is approximately given by: 3.142\n"
|
||||
```
|
||||
|
||||
## The crypto module
|
||||
|
||||
[The `:crypto` module](`:crypto`) contains hashing functions, digital signatures, encryption and more:
|
||||
|
||||
```elixir
|
||||
iex> Base.encode16(:crypto.hash(:sha256, "Elixir"))
|
||||
"3315715A7A3AD57428298676C5AE465DADA38D951BDFAC9348A8A31E9C7401CB"
|
||||
```
|
||||
|
||||
The `:crypto` module is part of the `:crypto` application that ships with Erlang. This means you must list the `:crypto` application as an additional application in your project configuration. To do this, edit your `mix.exs` file to include:
|
||||
|
||||
```elixir
|
||||
def application do
|
||||
[extra_applications: [:crypto]]
|
||||
end
|
||||
```
|
||||
|
||||
Any module that is not part of the `:kernel` or `:stdlib` Erlang applications must have their application explicitly listed in your `mix.exs`. You can find the application name of any Erlang module in the Erlang documentation, immediately below the Erlang logo in the sidebar.
|
||||
|
||||
## The digraph module
|
||||
|
||||
The [`:digraph`](`:digraph`) and [`:digraph_utils`](`:digraph_utils`) modules contain functions for dealing with directed graphs built of vertices and edges. After constructing the graph, the algorithms in there will help find, for instance, the shortest path between two vertices, or loops in the graph.
|
||||
|
||||
Given three vertices, find the shortest path from the first to the last.
|
||||
|
||||
```elixir
|
||||
iex> digraph = :digraph.new()
|
||||
iex> coords = [{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}]
|
||||
iex> [v0, v1, v2] = (for c <- coords, do: :digraph.add_vertex(digraph, c))
|
||||
iex> :digraph.add_edge(digraph, v0, v1)
|
||||
iex> :digraph.add_edge(digraph, v1, v2)
|
||||
iex> :digraph.get_short_path(digraph, v0, v2)
|
||||
[{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}]
|
||||
```
|
||||
|
||||
Note that the functions in `:digraph` alter the graph structure in-place, this
|
||||
is possible because they are implemented as ETS tables, explained next.
|
||||
|
||||
## Erlang Term Storage
|
||||
|
||||
The modules [`:ets`](`:ets`) and [`:dets`](`:dets`) handle storage of large data structures in memory or on disk respectively.
|
||||
|
||||
ETS lets you create a table containing tuples. By default, ETS tables are protected, which means only the owner process may write to the table but any other process can read. ETS has some functionality to allow a table to be used as a simple database, a key-value store or as a cache mechanism.
|
||||
|
||||
The functions in the `ets` module will modify the state of the table as a side-effect.
|
||||
|
||||
```elixir
|
||||
iex> table = :ets.new(:ets_test, [])
|
||||
# Store as tuples with {name, population}
|
||||
iex> :ets.insert(table, {"China", 1_374_000_000})
|
||||
iex> :ets.insert(table, {"India", 1_284_000_000})
|
||||
iex> :ets.insert(table, {"USA", 322_000_000})
|
||||
iex> :ets.i(table)
|
||||
<1 > {<<"India">>,1284000000}
|
||||
<2 > {<<"USA">>,322000000}
|
||||
<3 > {<<"China">>,1374000000}
|
||||
```
|
||||
|
||||
## The math module
|
||||
|
||||
The [`:math`](`:math`) module contains common mathematical operations covering trigonometry, exponential, and logarithmic functions.
|
||||
|
||||
```elixir
|
||||
iex> angle_45_deg = :math.pi() * 45.0 / 180.0
|
||||
iex> :math.sin(angle_45_deg)
|
||||
0.7071067811865475
|
||||
iex> :math.exp(55.0)
|
||||
7.694785265142018e23
|
||||
iex> :math.log(7.694785265142018e23)
|
||||
55.0
|
||||
```
|
||||
|
||||
## The queue module
|
||||
|
||||
The [`:queue`](`:queue`) module provides a data structure that implements (double-ended) FIFO (first-in first-out) queues efficiently:
|
||||
|
||||
```elixir
|
||||
iex> q = :queue.new
|
||||
iex> q = :queue.in("A", q)
|
||||
iex> q = :queue.in("B", q)
|
||||
iex> {value, q} = :queue.out(q)
|
||||
iex> value
|
||||
{:value, "A"}
|
||||
iex> {value, q} = :queue.out(q)
|
||||
iex> value
|
||||
{:value, "B"}
|
||||
iex> {value, q} = :queue.out(q)
|
||||
iex> value
|
||||
:empty
|
||||
```
|
||||
|
||||
## The rand module
|
||||
|
||||
The [`:rand`](`:rand`) has functions for returning random values and setting the random seed.
|
||||
|
||||
```elixir
|
||||
iex> :rand.uniform()
|
||||
0.8175669086010815
|
||||
iex> _ = :rand.seed(:exs1024, {123, 123534, 345345})
|
||||
iex> :rand.uniform()
|
||||
0.5820506340260994
|
||||
iex> :rand.uniform(6)
|
||||
6
|
||||
```
|
||||
|
||||
## The zip and zlib modules
|
||||
|
||||
The [`:zip`](`:zip`) module lets you read and write ZIP files to and from disk or memory, as well as extracting file information.
|
||||
|
||||
This code counts the number of files in a ZIP file:
|
||||
|
||||
```elixir
|
||||
iex> :zip.foldl(fn _, _, _, acc -> acc + 1 end, 0, :binary.bin_to_list("file.zip"))
|
||||
{:ok, 633}
|
||||
```
|
||||
|
||||
The [`:zlib`](`:zlib`) module deals with data compression in zlib format, as found in the `gzip` command line utility found in Unix systems.
|
||||
|
||||
```elixir
|
||||
iex> song = "
|
||||
...> Mary had a little lamb,
|
||||
...> His fleece was white as snow,
|
||||
...> And everywhere that Mary went,
|
||||
...> The lamb was sure to go."
|
||||
iex> compressed = :zlib.compress(song)
|
||||
iex> byte_size(song)
|
||||
110
|
||||
iex> byte_size(compressed)
|
||||
99
|
||||
iex> :zlib.uncompress(compressed)
|
||||
"\nMary had a little lamb,\nHis fleece was white as snow,\nAnd everywhere that Mary went,\nThe lamb was sure to go."
|
||||
```
|
||||
|
||||
## Learning Erlang
|
||||
|
||||
If you want to get deeper into Erlang, here's a list of online resources that cover Erlang's fundamentals and its more advanced features:
|
||||
|
||||
* This [Erlang Syntax: A Crash Course](https://elixir-lang.org/crash-course.html) provides a concise intro to Erlang's syntax. Each code snippet is accompanied by equivalent code in Elixir. This is an opportunity for you to not only get some exposure to Erlang's syntax but also review what you learned about Elixir.
|
||||
|
||||
* Erlang's official website has a short [tutorial](https://www.erlang.org/course). There is a chapter with pictures briefly describing Erlang's primitives for [concurrent programming](https://www.erlang.org/course/concurrent_programming.html).
|
||||
|
||||
* [Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/) is an excellent introduction to Erlang, its design principles, standard library, best practices, and much more. Once you have read through the crash course mentioned above, you'll be able to safely skip the first couple of chapters in the book that mostly deal with the syntax. When you reach [The Hitchhiker's Guide to Concurrency](http://learnyousomeerlang.com/the-hitchhikers-guide-to-concurrency) chapter, that's where the real fun starts.
|
||||
|
||||
Our last step is to take a look at existing Elixir (and Erlang) libraries you might use while debugging.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Introduction
|
||||
|
||||
Welcome!
|
||||
|
||||
This guide will teach you about Elixir fundamentals - the language syntax, how to define modules, the common data structures in the language, and more. This chapter will focus on ensuring that Elixir is installed and that you can successfully run Elixir's Interactive Shell, called IEx.
|
||||
|
||||
Let's get started.
|
||||
|
||||
## Installation
|
||||
|
||||
If you haven't yet installed Elixir, visit our [installation page](https://elixir-lang.org/install.html). Once you are done, you can run `elixir --version` to get the current Elixir version. The requirements for this guide are:
|
||||
|
||||
* Elixir 1.15.0 onwards
|
||||
* Erlang/OTP 26 onwards
|
||||
|
||||
If you are looking for other resources for learning Elixir, you can also consult the [learning page](https://elixir-lang.org/learning.html) of the official website.
|
||||
|
||||
## Interactive mode
|
||||
|
||||
When you install Elixir, you will have three new command line executables: `iex`, `elixir` and `elixirc`.
|
||||
|
||||
For now, let's start by running `iex` (or `iex.bat` if you are on Windows PowerShell, where `iex` is a PowerShell command) which stands for Interactive Elixir. In interactive mode, we can type any Elixir expression and get its result. Let's warm up with some basic expressions.
|
||||
|
||||
Open up `iex` and type the following expressions:
|
||||
|
||||
```elixir
|
||||
Erlang/OTP 26 [64-bit] [smp:2:2] [...]
|
||||
|
||||
Interactive Elixir - press Ctrl+C to exit
|
||||
iex(1)> 40 + 2
|
||||
42
|
||||
iex(2)> "hello" <> " world"
|
||||
"hello world"
|
||||
```
|
||||
|
||||
Please note that some details like version numbers may differ a bit in your session, that's not important. By executing the code above, you should evaluate expressions and see their results. To exit `iex` press `Ctrl+C` twice.
|
||||
|
||||
It seems we are ready to go! We will use the interactive shell quite a lot in the next chapters to get a bit more familiar with the language constructs and basic types, starting in the next chapter.
|
||||
|
||||
> Note: if you are on Windows and running on an Erlang/OTP version earlier than 26, you can also try `iex --werl` (`iex.bat --werl` on PowerShell) which may provide a better experience depending on which console you are using.
|
||||
|
||||
## Running scripts
|
||||
|
||||
After getting familiar with the basics of the language you may want to try writing simple programs. This can be accomplished by putting the following Elixir code into a file:
|
||||
|
||||
```elixir
|
||||
IO.puts("Hello world from Elixir")
|
||||
```
|
||||
|
||||
Save it as `simple.exs` and execute it with `elixir`:
|
||||
|
||||
```console
|
||||
$ elixir simple.exs
|
||||
Hello world from Elixir
|
||||
```
|
||||
|
||||
Later on we will learn [how to compile Elixir code](modules-and-functions.md) and how to create and work within Elixir projects using the Mix build tool. For now, let's move on to learn the basic data types in the language.
|
||||
@@ -0,0 +1,217 @@
|
||||
# IO and the file system
|
||||
|
||||
This chapter introduces the input/output mechanisms, file-system-related tasks, and related modules such as `IO`, `File`, and `Path`. The IO system provides a great opportunity to shed some light on some philosophies and curiosities of Elixir and the Erlang VM.
|
||||
|
||||
## The `IO` module
|
||||
|
||||
The `IO` module is the main mechanism in Elixir for reading and writing to standard input/output (`:stdio`), standard error (`:stderr`), files, and other IO devices. Usage of the module is pretty straightforward:
|
||||
|
||||
```elixir
|
||||
iex> IO.puts("hello world")
|
||||
hello world
|
||||
:ok
|
||||
iex> IO.gets("yes or no? ")
|
||||
yes or no? yes
|
||||
"yes\n"
|
||||
```
|
||||
|
||||
By default, functions in the `IO` module read from the standard input and write to the standard output. We can change that by passing, for example, `:stderr` as an argument (in order to write to the standard error device):
|
||||
|
||||
```elixir
|
||||
iex> IO.puts(:stderr, "hello world")
|
||||
hello world
|
||||
:ok
|
||||
```
|
||||
|
||||
## The `File` module
|
||||
|
||||
The `File` module contains functions that allow us to open files as IO devices. By default, files are opened in binary mode, which requires developers to use the specific `IO.binread/2` and `IO.binwrite/2` functions from the `IO` module:
|
||||
|
||||
```elixir
|
||||
iex> {:ok, file} = File.open("path/to/file/hello", [:write])
|
||||
{:ok, #PID<0.47.0>}
|
||||
iex> IO.binwrite(file, "world")
|
||||
:ok
|
||||
iex> File.close(file)
|
||||
:ok
|
||||
iex> File.read("path/to/file/hello")
|
||||
{:ok, "world"}
|
||||
```
|
||||
|
||||
A file can also be opened with `:utf8` encoding, which tells the `File` module to interpret the bytes read from the file as UTF-8-encoded bytes.
|
||||
|
||||
Besides functions for opening, reading and writing files, the `File` module has many functions to work with the file system. Those functions are named after their UNIX equivalents. For example, `File.rm/1` can be used to remove files, `File.mkdir/1` to create directories, `File.mkdir_p/1` to create directories and all their parent chain. There are even `File.cp_r/2` and `File.rm_rf/1` to respectively copy and remove files and directories recursively (i.e., copying and removing the contents of the directories too).
|
||||
|
||||
You will also notice that functions in the `File` module have two variants: one "regular" variant and another variant with a trailing bang (`!`). For example, when we read the `"hello"` file in the example above, we use `File.read/1`. Alternatively, we can use `File.read!/1`:
|
||||
|
||||
```elixir
|
||||
iex> File.read("path/to/file/hello")
|
||||
{:ok, "world"}
|
||||
iex> File.read!("path/to/file/hello")
|
||||
"world"
|
||||
iex> File.read("path/to/file/unknown")
|
||||
{:error, :enoent}
|
||||
iex> File.read!("path/to/file/unknown")
|
||||
** (File.Error) could not read file "path/to/file/unknown": no such file or directory
|
||||
```
|
||||
|
||||
Notice that the version with `!` returns the contents of the file instead of a tuple, and if anything goes wrong the function raises an error.
|
||||
|
||||
The version without `!` is preferred when you want to handle different outcomes using pattern matching:
|
||||
|
||||
```elixir
|
||||
case File.read("path/to/file/hello") do
|
||||
{:ok, body} -> # do something with the `body`
|
||||
{:error, reason} -> # handle the error caused by `reason`
|
||||
end
|
||||
```
|
||||
|
||||
However, if you expect the file to be there, the bang variation is more useful as it raises a meaningful error message. Avoid writing:
|
||||
|
||||
```elixir
|
||||
{:ok, body} = File.read("path/to/file/unknown")
|
||||
```
|
||||
|
||||
as, in case of an error, `File.read/1` will return `{:error, reason}` and the pattern matching will fail. You will still get the desired result (a raised error), but the message will be about the pattern which doesn't match (thus being cryptic in respect to what the error actually is about).
|
||||
|
||||
Therefore, if you don't want to handle the error outcomes, prefer to use the functions ending with an exclamation mark, such as `File.read!/1`.
|
||||
|
||||
## The `Path` module
|
||||
|
||||
The majority of the functions in the `File` module expect paths as arguments. Most commonly, those paths will be regular binaries. The `Path` module provides facilities for working with such paths:
|
||||
|
||||
```elixir
|
||||
iex> Path.join("foo", "bar")
|
||||
"foo/bar"
|
||||
iex> Path.expand("~/hello")
|
||||
"/Users/jose/hello"
|
||||
```
|
||||
|
||||
Using functions from the `Path` module as opposed to directly manipulating strings is preferred since the `Path` module takes care of different operating systems transparently. Finally, keep in mind that Elixir will automatically convert slashes (`/`) into backslashes (`\`) on Windows when performing file operations.
|
||||
|
||||
With this, we have covered the main modules that Elixir provides for dealing with IO and interacting with the file system. In the next section, we will peek a bit under the covers and learn how the IO system is implemented in the VM.
|
||||
|
||||
## Processes
|
||||
|
||||
You may have noticed that `File.open/2` returns a tuple like `{:ok, pid}`:
|
||||
|
||||
```elixir
|
||||
iex> {:ok, file} = File.open("hello", [:write])
|
||||
{:ok, #PID<0.47.0>}
|
||||
```
|
||||
|
||||
This happens because the `IO` module actually works with processes (see [the previous chapter](processes.md)). Given a file is a process, when you write to a file that has been closed, you are actually sending a message to a process which has been terminated:
|
||||
|
||||
```elixir
|
||||
iex> File.close(file)
|
||||
:ok
|
||||
iex> IO.write(file, "is anybody out there")
|
||||
** (ErlangError) Erlang error: :terminated:
|
||||
|
||||
* 1st argument: the device has terminated
|
||||
|
||||
(stdlib 5.0) io.erl:94: :io.put_chars(#PID<0.114.0>, "is anybody out there")
|
||||
iex:4: (file)
|
||||
```
|
||||
|
||||
Let's see in more detail what happens when you request `IO.write(pid, binary)`. The `IO` module sends a message to the process identified by `pid` with the desired operation. A small ad-hoc process can help us see it:
|
||||
|
||||
```elixir
|
||||
iex> pid = spawn(fn ->
|
||||
...> receive do: (msg -> IO.inspect(msg))
|
||||
...> end)
|
||||
#PID<0.57.0>
|
||||
iex> IO.write(pid, "hello")
|
||||
{:io_request, #PID<0.41.0>, #Reference<0.0.8.91>,
|
||||
{:put_chars, :unicode, "hello"}}
|
||||
** (ErlangError) erlang error: :terminated
|
||||
```
|
||||
|
||||
After `IO.write/2`, we can see the request sent by the `IO` module printed out (a four-elements tuple). Soon after that, we see that it fails since the `IO` module expected some kind of result, which we did not supply.
|
||||
|
||||
By modeling IO devices with processes, the Erlang VM allows us to even read and write to files across nodes. Neat!
|
||||
|
||||
## `iodata` and `chardata`
|
||||
|
||||
In all of the examples above, we used binaries when writing to files. However, most of the IO functions in Elixir also accept either "iodata" or "chardata".
|
||||
|
||||
One of the main reasons for using "iodata" and "chardata" is for performance. For example,
|
||||
imagine you need to greet someone in your application:
|
||||
|
||||
```elixir
|
||||
name = "Mary"
|
||||
IO.puts("Hello " <> name <> "!")
|
||||
```
|
||||
|
||||
Given strings in Elixir are immutable, as most data structures, the example above will copy the string "Mary" into the new "Hello Mary!" string. While this is unlikely to matter for the short string as above, copying can be quite expensive for large strings! For this reason, the IO functions in Elixir allow you to pass instead a list of strings:
|
||||
|
||||
```elixir
|
||||
name = "Mary"
|
||||
IO.puts(["Hello ", name, "!"])
|
||||
```
|
||||
|
||||
In the example above, there is no copying. Instead we create a list that contains the original name. We call such lists either "iodata" or "chardata" and we will learn the precise difference between them soon.
|
||||
|
||||
Those lists are very useful because it can actually simplify the processing strings in several scenarios. For example, imagine you have a list of values, such as `["apple", "banana", "lemon"]` that you want to write to disk separated by commas. How can you achieve this?
|
||||
|
||||
One option is to use `Enum.join/2` and convert the values to a string:
|
||||
|
||||
```elixir
|
||||
iex> Enum.join(["apple", "banana", "lemon"], ",")
|
||||
"apple,banana,lemon"
|
||||
```
|
||||
|
||||
The above returns a new string by copying each value into the new string. However, with the knowledge in this section, we know that we can pass a list of strings to the IO/File functions. So instead we can do:
|
||||
|
||||
```elixir
|
||||
iex> Enum.intersperse(["apple", "banana", "lemon"], ",")
|
||||
["apple", ",", "banana", ",", "lemon"]
|
||||
```
|
||||
|
||||
"iodata" and "chardata" do not only contain strings, but they may contain arbitrary nested lists of strings too:
|
||||
|
||||
```elixir
|
||||
iex> IO.puts(["apple", [",", "banana", [",", "lemon"]]])
|
||||
```
|
||||
|
||||
"iodata" and "chardata" may also contain integers. For example, we could print our comma separated list of values by using `?,` as separator, which is the integer representing a comma (`44`):
|
||||
|
||||
```elixir
|
||||
iex> IO.puts(["apple", ?,, "banana", ?,, "lemon"])
|
||||
```
|
||||
|
||||
The difference between "iodata" and "chardata" is precisely what said integer represents. For iodata, the integers represent bytes. For chardata, the integers represent Unicode codepoints. For ASCII characters, the byte representation is the same as the codepoint representation, so it fits both classifications. However, the default IO device works with chardata, which means we can do:
|
||||
|
||||
```elixir
|
||||
iex> IO.puts([?O, ?l, ?á, ?\s, "Mary", ?!])
|
||||
```
|
||||
|
||||
Overall, integers in a list may represent either a bunch of bytes or a bunch of characters and which one to use depends on the encoding of the IO device. If the file is opened without encoding, the file is expected to be in raw mode, and the functions in the `IO` module starting with `bin*` must be used. Those functions expect an `iodata` as an argument, where integers in the list would represent bytes.
|
||||
|
||||
On the other hand, the default IO device (`:stdio`) and files opened with `:utf8` encoding work with the remaining functions in the `IO` module. Those functions expect a `chardata` as an argument, where integers represent codepoints.
|
||||
|
||||
Although this is a subtle difference, you only need to worry about these details if you intend to pass lists containing integers to those functions. If you pass binaries, or list of binaries, then there is no ambiguity.
|
||||
|
||||
Finally, there is one last construct called charlist, which [we discussed in earlier chapters](binaries-strings-and-charlists.md). Charlists are a special case of chardata where all values are integers representing Unicode codepoints. They can be created with the `~c` sigil:
|
||||
|
||||
```elixir
|
||||
iex> ~c"hello"
|
||||
~c"hello"
|
||||
```
|
||||
|
||||
Charlists mostly show up when interfacing with Erlang, as some Erlang APIs use charlist as their representation for strings. For this reason, any list containing printable ASCII codepoints will be printed as a charlist:
|
||||
|
||||
```elixir
|
||||
iex> [?a, ?b, ?c]
|
||||
~c"abc"
|
||||
```
|
||||
|
||||
We packed a lot into this small section, so let's break it down:
|
||||
|
||||
* iodata and chardata are lists of binaries and integers. Those binaries and integers can be arbitrarily nested inside lists. Their goal is to give flexibility and performance when working with IO devices and files;
|
||||
|
||||
* the choice between iodata and chardata depends on the encoding of the IO device. If the file is opened without encoding, the file expects iodata, and the functions in the `IO` module starting with `bin*` must be used. The default IO device (`:stdio`) and files opened with `:utf8` encoding expect chardata and work with the remaining functions in the `IO` module;
|
||||
|
||||
* charlists are a special case of chardata, where it exclusively uses a list of integers Unicode codepoints. They can be created with the `~c` sigil. Lists of integers are automatically printed using the `~c` sigil if all integers in a list represent printable ASCII codepoints.
|
||||
|
||||
This finishes our tour of IO devices and IO related functionality. We have learned about three Elixir modules - `IO`, `File`, and `Path` - as well as how the VM uses processes for the underlying IO mechanisms and how to use `chardata` and `iodata` for IO operations.
|
||||
@@ -0,0 +1,276 @@
|
||||
# Keyword lists and maps
|
||||
|
||||
Now let's talk about associative data structures. Associative data structures are able to associate a key to a certain value. Different languages call these different names like dictionaries, hashes, associative arrays, etc.
|
||||
|
||||
In Elixir, we have two main associative data structures: keyword lists and maps.
|
||||
|
||||
## Keyword lists
|
||||
|
||||
Keyword lists are a data-structure used to pass options to functions. Imagine you want to split a string of numbers. We can use `String.split/2`:
|
||||
|
||||
```elixir
|
||||
iex> String.split("1 2 3", " ")
|
||||
["1", "2", "3"]
|
||||
```
|
||||
|
||||
However, what happens if there is an additional space between the numbers:
|
||||
|
||||
```elixir
|
||||
iex> String.split("1 2 3", " ")
|
||||
["1", "", "2", "", "3"]
|
||||
```
|
||||
|
||||
As you can see, there are now empty strings in our results. Luckily, the `String.split/3` function allows the `trim` option to be set to true:
|
||||
|
||||
```elixir
|
||||
iex> String.split("1 2 3", " ", [trim: true])
|
||||
["1", "2", "3"]
|
||||
```
|
||||
|
||||
`[trim: true]` is a keyword list. Furthermore, when a keyword list is the last argument of a function, we can skip the brackets and write:
|
||||
|
||||
```elixir
|
||||
iex> String.split("1 2 3", " ", trim: true)
|
||||
["1", "2", "3"]
|
||||
```
|
||||
|
||||
As shown in the example above, keyword lists are mostly used as optional arguments to functions.
|
||||
|
||||
As the name implies, keyword lists are simply lists. In particular, they are lists consisting of 2-item tuples where the first element (the key) is an atom and the second element can be any value. Both representations are the same:
|
||||
|
||||
```elixir
|
||||
iex> [{:trim, true}] == [trim: true]
|
||||
true
|
||||
```
|
||||
|
||||
Since keyword lists are lists, we can use all operations available to lists. For example, we can use `++` to add new values to a keyword list:
|
||||
|
||||
```elixir
|
||||
iex> list = [a: 1, b: 2]
|
||||
[a: 1, b: 2]
|
||||
iex> list ++ [c: 3]
|
||||
[a: 1, b: 2, c: 3]
|
||||
iex> [a: 0] ++ list
|
||||
[a: 0, a: 1, b: 2]
|
||||
```
|
||||
|
||||
You can read the value of a keyword list using the brackets syntax. This is also known as the access syntax, as it is defined by the `Access` module:
|
||||
|
||||
```elixir
|
||||
iex> list[:a]
|
||||
1
|
||||
iex> list[:b]
|
||||
2
|
||||
```
|
||||
|
||||
In case of duplicate keys, values added to the front are the ones fetched:
|
||||
|
||||
```elixir
|
||||
iex> new_list = [a: 0] ++ list
|
||||
[a: 0, a: 1, b: 2]
|
||||
iex> new_list[:a]
|
||||
0
|
||||
```
|
||||
|
||||
Keyword lists are important because they have three special characteristics:
|
||||
|
||||
* Keys must be atoms.
|
||||
* Keys are ordered, as specified by the developer.
|
||||
* Keys can be given more than once.
|
||||
|
||||
For example, [the Ecto library](https://github.com/elixir-lang/ecto) makes use of these features to provide an elegant DSL for writing database queries:
|
||||
|
||||
```elixir
|
||||
query =
|
||||
from w in Weather,
|
||||
where: w.prcp > 0,
|
||||
where: w.temp < 20,
|
||||
select: w
|
||||
```
|
||||
|
||||
Although we can pattern match on keyword lists, it is not done in practice since pattern matching on lists requires the number of items and their order to match:
|
||||
|
||||
```elixir
|
||||
iex> [a: a] = [a: 1]
|
||||
[a: 1]
|
||||
iex> a
|
||||
1
|
||||
iex> [a: a] = [a: 1, b: 2]
|
||||
** (MatchError) no match of right hand side value: [a: 1, b: 2]
|
||||
iex> [b: b, a: a] = [a: 1, b: 2]
|
||||
** (MatchError) no match of right hand side value: [a: 1, b: 2]
|
||||
```
|
||||
|
||||
Furthermore, given keyword lists are often used as optional arguments, they are used in situations where not all keys may be present, which would make it impossible to match on them. In a nutshell, do not pattern match on keyword lists.
|
||||
|
||||
In order to manipulate keyword lists, Elixir provides the `Keyword` module. Remember, though, keyword lists are simply lists, and as such they provide the same linear performance characteristics as them: the longer the list, the longer it will take to find a key, to count the number of items, and so on. If you need to store a large amount of keys in a key-value data structure, Elixir offers maps, which we will soon learn.
|
||||
|
||||
### `do`-blocks and keywords
|
||||
|
||||
As we have seen, keywords are mostly used in the language to pass optional values. In fact, we have used keywords before in this guide. For example, we have seen:
|
||||
|
||||
```elixir
|
||||
iex> if true do
|
||||
...> "This will be seen"
|
||||
...> else
|
||||
...> "This won't"
|
||||
...> end
|
||||
"This will be seen"
|
||||
```
|
||||
|
||||
It happens that `do` blocks are nothing more than a syntax convenience on top of keywords. We can rewrite the above to:
|
||||
|
||||
```elixir
|
||||
iex> if true, do: "This will be seen", else: "This won't"
|
||||
"This will be seen"
|
||||
```
|
||||
|
||||
Pay close attention to both syntaxes. In the keyword list format, we separate each key-value pair with commas, and each key is followed by `:`. In the `do`-blocks, we get rid of the colons, the commas, and separate each keyword by a newline. They are useful exactly because they remove the verbosity when writing blocks of code. Most of the time, you will use the block syntax, but it is good to know they are equivalent.
|
||||
|
||||
This plays an important role in the language as it allows Elixir syntax to stay small but still expressive. We only need few data structures to represent the language, a topic we will come back to when talking about [optional syntax](optional-syntax.md) and go in-depth when discussing [meta-programming](../meta-programming/quote-and-unquote.md).
|
||||
|
||||
With this out of the way, let's talk about maps.
|
||||
|
||||
## Maps as key-value pairs
|
||||
|
||||
Whenever you need to store key-value pairs, maps are the "go to" data structure in Elixir. A map is created using the `%{}` syntax:
|
||||
|
||||
```elixir
|
||||
iex> map = %{:a => 1, 2 => :b}
|
||||
%{2 => :b, :a => 1}
|
||||
iex> map[:a]
|
||||
1
|
||||
iex> map[2]
|
||||
:b
|
||||
iex> map[:c]
|
||||
nil
|
||||
```
|
||||
|
||||
Compared to keyword lists, we can already see two differences:
|
||||
|
||||
* Maps allow any value as a key.
|
||||
* Maps' keys do not follow any ordering.
|
||||
|
||||
In contrast to keyword lists, maps are very useful with pattern matching. When a map is used in a pattern, it will always match on a subset of the given value:
|
||||
|
||||
```elixir
|
||||
iex> %{} = %{:a => 1, 2 => :b}
|
||||
%{2 => :b, :a => 1}
|
||||
iex> %{:a => a} = %{:a => 1, 2 => :b}
|
||||
%{2 => :b, :a => 1}
|
||||
iex> a
|
||||
1
|
||||
iex> %{:c => c} = %{:a => 1, 2 => :b}
|
||||
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
|
||||
```
|
||||
|
||||
As shown above, a map matches as long as the keys in the pattern exist in the given map. Therefore, an empty map matches all maps.
|
||||
|
||||
The `Map` module provides a very similar API to the `Keyword` module with convenience functions to add, remove, and update maps keys:
|
||||
|
||||
```elixir
|
||||
iex> Map.get(%{:a => 1, 2 => :b}, :a)
|
||||
1
|
||||
iex> Map.put(%{:a => 1, 2 => :b}, :c, 3)
|
||||
%{2 => :b, :a => 1, :c => 3}
|
||||
iex> Map.to_list(%{:a => 1, 2 => :b})
|
||||
[{2, :b}, {:a, 1}]
|
||||
```
|
||||
|
||||
## Maps of predefined keys
|
||||
|
||||
In the previous section, we have used maps as a key-value data structure where keys can be added or removed at any time. However, it is also common to create maps with a pre-defined set of keys. Their values may be updated, but new keys are never added nor removed. This is useful when we know the shape of the data we are working with and, if we get a different key, it likely means a mistake was done elsewhere.
|
||||
|
||||
We define such maps using the same syntax as in the previous section, except that all keys must be atoms:
|
||||
|
||||
```elixir
|
||||
iex> map = %{:name => "John", :age => 23}
|
||||
%{name: "John", age: 23}
|
||||
```
|
||||
|
||||
As you can see from the printed result above, Elixir also allows you to write maps of atom keys using the same `key: value` syntax as keyword lists.
|
||||
|
||||
When the keys are atoms, in particular when working with maps of predefined keys, we can also access them using the `map.key` syntax:
|
||||
|
||||
```elixir
|
||||
iex> map = %{name: "John", age: 23}
|
||||
%{name: "John", age: 23}
|
||||
|
||||
iex> map.name
|
||||
"John"
|
||||
iex> map.agee
|
||||
** (KeyError) key :agee not found in: %{name: "John", age: 23}
|
||||
```
|
||||
|
||||
There is also syntax for updating keys, which also raises if the key has not yet been defined:
|
||||
|
||||
```elixir
|
||||
iex> %{map | name: "Mary"}
|
||||
%{name: "Mary", age: 23}
|
||||
iex> %{map | agee: 27}
|
||||
** (KeyError) key :agee not found in: %{name: "John", age: 23}
|
||||
```
|
||||
|
||||
These operations have one large benefit in that they raise if the key does not exist in the map and the compiler may even detect and warn when possible. This makes them useful to get quick feedback and spot bugs and typos early on. This is also the syntax used to power another Elixir feature called "Structs", which we will learn later on.
|
||||
|
||||
Elixir developers typically prefer to use the `map.key` syntax and pattern matching instead of the functions in the `Map` module when working with maps because they lead to an assertive style of programming. [This blog post by José Valim](https://dashbit.co/blog/writing-assertive-code-with-elixir) provides insight and examples on how you get more concise and faster software by writing assertive code in Elixir.
|
||||
|
||||
## Nested data structures
|
||||
|
||||
Often we will have maps inside maps, or even keywords lists inside maps, and so forth. Elixir provides conveniences for manipulating nested data structures via the `put_in/2`, `update_in/2` and other macros giving the same conveniences you would find in imperative languages while keeping the immutable properties of the language.
|
||||
|
||||
Imagine you have the following structure:
|
||||
|
||||
```elixir
|
||||
iex> users = [
|
||||
john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
|
||||
mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
|
||||
]
|
||||
[
|
||||
john: %{age: 27, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
|
||||
mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}
|
||||
]
|
||||
```
|
||||
|
||||
We have a keyword list of users where each value is a map containing the name, age and a list of programming languages each user likes. If we wanted to access the age for john, we could write:
|
||||
|
||||
```elixir
|
||||
iex> users[:john].age
|
||||
27
|
||||
```
|
||||
|
||||
It happens we can also use this same syntax for updating the value:
|
||||
|
||||
```elixir
|
||||
iex> users = put_in users[:john].age, 31
|
||||
[
|
||||
john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
|
||||
mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}
|
||||
]
|
||||
```
|
||||
|
||||
The `update_in/2` macro is similar but allows us to pass a function that controls how the value changes. For example, let's remove "Clojure" from Mary's list of languages:
|
||||
|
||||
```elixir
|
||||
iex> users = update_in users[:mary].languages, fn languages -> List.delete(languages, "Clojure") end
|
||||
[
|
||||
john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
|
||||
mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}
|
||||
]
|
||||
```
|
||||
|
||||
There is more to learn about `put_in/2` and `update_in/2`, including the `get_and_update_in/2` that allows us to extract a value and update the data structure at once. There are also `put_in/3`, `update_in/3` and `get_and_update_in/3` which allow dynamic access into the data structure.
|
||||
|
||||
## Summary
|
||||
|
||||
There are two different data structures for working with key-value stores in Elixir. Alongside the `Access` module and pattern matching, they provide a rich set of tools for manipulating complex, potentially nested, data structures.
|
||||
|
||||
As we conclude this chapter, the important to keep in mind is that you should:
|
||||
|
||||
* Use keyword lists for passing optional values to functions
|
||||
|
||||
* Use maps for general key-value data structures
|
||||
|
||||
* Use maps when working with data that has a predefined set of keys
|
||||
|
||||
Now let's talk about modules and functions.
|
||||
@@ -0,0 +1,192 @@
|
||||
# Lists and tuples
|
||||
|
||||
In this chapter we will learn two of the most used collection data-types in Elixir: lists and tuples.
|
||||
|
||||
## (Linked) Lists
|
||||
|
||||
Elixir uses square brackets to specify a list of values. Values can be of any type:
|
||||
|
||||
```elixir
|
||||
iex> [1, 2, true, 3]
|
||||
[1, 2, true, 3]
|
||||
iex> length([1, 2, 3])
|
||||
3
|
||||
```
|
||||
|
||||
Two lists can be concatenated or subtracted using the `++/2` and `--/2` operators respectively:
|
||||
|
||||
```elixir
|
||||
iex> [1, 2, 3] ++ [4, 5, 6]
|
||||
[1, 2, 3, 4, 5, 6]
|
||||
iex> [1, true, 2, false, 3, true] -- [true, false]
|
||||
[1, 2, 3, true]
|
||||
```
|
||||
|
||||
List operators never modify the existing list. Concatenating to or removing elements from a list returns a new list. We say that Elixir data structures are *immutable*. One advantage of immutability is that it leads to clearer code. You can freely pass the data around with the guarantee no one will mutate it in memory - only transform it.
|
||||
|
||||
Throughout the tutorial, we will talk a lot about the head and tail of a list. The head is the first element of a list and the tail is the remainder of the list. They can be retrieved with the functions `hd/1` and `tl/1`. Let's assign a list to a variable and retrieve its head and tail:
|
||||
|
||||
```elixir
|
||||
iex> list = [1, 2, 3]
|
||||
iex> hd(list)
|
||||
1
|
||||
iex> tl(list)
|
||||
[2, 3]
|
||||
```
|
||||
|
||||
Getting the head or the tail of an empty list throws an error:
|
||||
|
||||
```elixir
|
||||
iex> hd([])
|
||||
** (ArgumentError) argument error
|
||||
```
|
||||
|
||||
Sometimes you will create a list and it will return a quoted value preceded by `~c`. For example:
|
||||
|
||||
```elixir
|
||||
iex> [11, 12, 13]
|
||||
~c"\v\f\r"
|
||||
iex> [104, 101, 108, 108, 111]
|
||||
~c"hello"
|
||||
```
|
||||
|
||||
When Elixir sees a list of printable ASCII numbers, Elixir will print that as a charlist (literally a list of characters). Charlists are quite common when interfacing with existing Erlang code. Whenever you see a value in IEx and you are not quite sure what it is, you can use the `i/1` to retrieve information about it:
|
||||
|
||||
```elixir
|
||||
iex> i ~c"hello"
|
||||
Term
|
||||
i ~c"hello"
|
||||
Data type
|
||||
List
|
||||
Description
|
||||
...
|
||||
Raw representation
|
||||
[104, 101, 108, 108, 111]
|
||||
Reference modules
|
||||
List
|
||||
Implemented protocols
|
||||
...
|
||||
```
|
||||
|
||||
We will talk more about charlists in the ["Binaries, strings, and charlists"](binaries-strings-and-charlists.md) chapter.
|
||||
|
||||
> #### Single-quoted strings {: .info}
|
||||
>
|
||||
> In Elixir, you can also use `'hello'` to build charlists, but this notation has been soft-deprecated in Elixir v1.15 and will emit warnings in future versions. Prefer to write `~c"hello"` instead.
|
||||
|
||||
## Tuples
|
||||
|
||||
Elixir uses curly brackets to define tuples. Like lists, tuples can hold any value:
|
||||
|
||||
```elixir
|
||||
iex> {:ok, "hello"}
|
||||
{:ok, "hello"}
|
||||
iex> tuple_size({:ok, "hello"})
|
||||
2
|
||||
```
|
||||
|
||||
Tuples store elements contiguously in memory. This means accessing a tuple element by index or getting the tuple size is a fast operation. Indexes start from zero:
|
||||
|
||||
```elixir
|
||||
iex> tuple = {:ok, "hello"}
|
||||
{:ok, "hello"}
|
||||
iex> elem(tuple, 1)
|
||||
"hello"
|
||||
iex> tuple_size(tuple)
|
||||
2
|
||||
```
|
||||
|
||||
It is also possible to put an element at a particular index in a tuple with `put_elem/3`:
|
||||
|
||||
```elixir
|
||||
iex> tuple = {:ok, "hello"}
|
||||
{:ok, "hello"}
|
||||
iex> put_elem(tuple, 1, "world")
|
||||
{:ok, "world"}
|
||||
iex> tuple
|
||||
{:ok, "hello"}
|
||||
```
|
||||
|
||||
Notice that `put_elem/3` returned a new tuple. The original tuple stored in the `tuple` variable was not modified. Like lists, tuples are also immutable. Every operation on a tuple returns a new tuple, it never changes the given one.
|
||||
|
||||
## Lists or tuples?
|
||||
|
||||
What is the difference between lists and tuples?
|
||||
|
||||
Lists are stored in memory as linked lists, meaning that each element in a list holds its value and points to the following element until the end of the list is reached. This means accessing the length of a list is a linear operation: we need to traverse the whole list in order to figure out its size.
|
||||
|
||||
Similarly, the performance of list concatenation depends on the length of the left-hand list:
|
||||
|
||||
```elixir
|
||||
iex> list = [1, 2, 3]
|
||||
[1, 2, 3]
|
||||
|
||||
# This is fast as we only need to traverse `[0]` to prepend to `list`
|
||||
iex> [0] ++ list
|
||||
[0, 1, 2, 3]
|
||||
|
||||
# This is slow as we need to traverse `list` to append 4
|
||||
iex> list ++ [4]
|
||||
[1, 2, 3, 4]
|
||||
```
|
||||
|
||||
Tuples, on the other hand, are stored contiguously in memory. This means getting the tuple size or accessing an element by index is fast. On the other hand, updating or adding elements to tuples is expensive because it requires creating a new tuple in memory:
|
||||
|
||||
```elixir
|
||||
iex> tuple = {:a, :b, :c, :d}
|
||||
{:a, :b, :c, :d}
|
||||
iex> put_elem(tuple, 2, :e)
|
||||
{:a, :b, :e, :d}
|
||||
```
|
||||
|
||||
Note, however, the elements themselves are not copied. When you update a tuple, all entries are shared between the old and the new tuple, except for the entry that has been replaced. This rule applies to most data structures in Elixir. This reduces the amount of memory allocation the language needs to perform and is only possible thanks to the immutable semantics of the language.
|
||||
|
||||
Those performance characteristics dictate the usage of those data structures. In a nutshell, lists are used when the number of elements returned may vary. Tuples have a fixed size. Let's see two examples from the `String` module:
|
||||
|
||||
```elixir
|
||||
iex> String.split("hello world")
|
||||
["hello", "world"]
|
||||
iex> String.split("hello beautiful world")
|
||||
["hello", "beautiful", "world"]
|
||||
```
|
||||
|
||||
The `String.split/2` function breaks a string into a list of strings on every whitespace character. Since the amount of elements returned depends on the input, we use a list.
|
||||
|
||||
On the other hand, `String.split_at/2` splits a string in two parts at a given position. Since it always returns two entries, regardless of the input size, it returns tuples:
|
||||
|
||||
```elixir
|
||||
iex> String.split_at("hello world", 3)
|
||||
{"hel", "lo world"}
|
||||
iex> String.split_at("hello world", -4)
|
||||
{"hello w", "orld"}
|
||||
```
|
||||
|
||||
It is also very common to use tuples and atoms to create "tagged tuples", which is a handy return value when an operation may succeed or fail. For example, `File.read/1` reads the contents of a file at a given path, which may or may not exist. It returns tagged tuples:
|
||||
|
||||
```elixir
|
||||
iex> File.read("path/to/existing/file")
|
||||
{:ok, "... contents ..."}
|
||||
iex> File.read("path/to/unknown/file")
|
||||
{:error, :enoent}
|
||||
```
|
||||
|
||||
If the path given to `File.read/1` exists, it returns a tuple with the atom `:ok` as the first element and the file contents as the second. Otherwise, it returns a tuple with `:error` and the error description. As we will soon learn, Elixir allows us to *pattern match* on tagged tuples and effortlessly handle both success and failure cases.
|
||||
|
||||
Given Elixir consistently follows those rules, the choice between lists and tuples get clearer as you learn and use the language. Elixir often guides you to do the right thing. For example, there is an `elem/2` function to access a tuple item:
|
||||
|
||||
```elixir
|
||||
iex> tuple = {:ok, "hello"}
|
||||
{:ok, "hello"}
|
||||
iex> elem(tuple, 1)
|
||||
"hello"
|
||||
```
|
||||
|
||||
However, given you often don't know the number of elements in a list, there is no built-in equivalent for accessing arbitrary entries in a lists, apart from its head.
|
||||
|
||||
## Size or length?
|
||||
|
||||
When counting the elements in a data structure, Elixir also abides by a simple rule: the function is named `size` if the operation is in constant time (the value is pre-calculated) or `length` if the operation is linear (calculating the length gets slower as the input grows). As a mnemonic, both "length" and "linear" start with "l".
|
||||
|
||||
For example, we have used 4 counting functions so far: `byte_size/1` (for the number of bytes in a string), `tuple_size/1` (for tuple size), `length/1` (for list length) and `String.length/1` (for the number of graphemes in a string). We use `byte_size` to get the number of bytes in a string, which is a cheap operation. Retrieving the number of Unicode graphemes, on the other hand, uses `String.length/1`, and may be expensive as it relies on a traversal of the entire string.
|
||||
|
||||
Now that we are familiar with the basic data-types in the language, let's learn important constructs for writing code, before we discuss more complex data structures.
|
||||
@@ -0,0 +1,184 @@
|
||||
# Module attributes
|
||||
|
||||
Module attributes in Elixir serve three purposes:
|
||||
|
||||
1. They serve to annotate the module, often with information to be used by the user or the VM.
|
||||
2. They work as constants.
|
||||
3. They work as a temporary module storage to be used during compilation.
|
||||
|
||||
Let's check each case, one by one.
|
||||
|
||||
## As annotations
|
||||
|
||||
Elixir brings the concept of module attributes from Erlang. For example:
|
||||
|
||||
```elixir
|
||||
defmodule MyServer do
|
||||
@moduledoc "My server code."
|
||||
end
|
||||
```
|
||||
|
||||
In the example above, we are defining the module documentation by using the module attribute syntax. Elixir has a handful of reserved attributes. Here are a few of them, the most commonly used ones:
|
||||
|
||||
* `@moduledoc` — provides documentation for the current module.
|
||||
* `@doc` — provides documentation for the function or macro that follows the attribute.
|
||||
* `@spec` — provides a typespec for the function that follows the attribute.
|
||||
* `@behaviour` — (notice the British spelling) used for specifying an OTP or user-defined behaviour.
|
||||
|
||||
`@moduledoc` and `@doc` are by far the most used attributes, and we expect you to use them a lot. Elixir treats documentation as first-class and provides many functions to access documentation. We will cover them [in their own chapter](writing-documentation.md).
|
||||
|
||||
Let's go back to the `Math` module defined in the previous chapters, add some documentation and save it to the `math.ex` file:
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
@moduledoc """
|
||||
Provides math-related functions.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Math.sum(1, 2)
|
||||
3
|
||||
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Calculates the sum of two numbers.
|
||||
"""
|
||||
def sum(a, b), do: a + b
|
||||
end
|
||||
```
|
||||
|
||||
Elixir promotes the use of Markdown with heredocs to write readable documentation. Heredocs are multi-line strings, they start and end with triple double-quotes, keeping the formatting of the inner text. We can access the documentation of any compiled module directly from IEx:
|
||||
|
||||
```console
|
||||
$ elixirc math.ex
|
||||
$ iex
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> h Math # Access the docs for the module Math
|
||||
...
|
||||
iex> h Math.sum # Access the docs for the sum function
|
||||
...
|
||||
```
|
||||
|
||||
We also provide a tool called [ExDoc](https://github.com/elixir-lang/ex_doc) which is used to generate HTML pages from the documentation.
|
||||
|
||||
You can take a look at the docs for `Module` for a complete list of supported attributes. Elixir also uses attributes to define [typespecs](../references/typespecs.md), which can be used to declare contracts between modules later.
|
||||
|
||||
## As "constants"
|
||||
|
||||
Elixir developers often use module attributes when they wish to make a value more visible or reusable:
|
||||
|
||||
```elixir
|
||||
defmodule MyServer do
|
||||
@initial_state %{host: "127.0.0.1", port: 3456}
|
||||
IO.inspect @initial_state
|
||||
end
|
||||
```
|
||||
|
||||
Trying to access an attribute that was not defined will print a warning:
|
||||
|
||||
```elixir
|
||||
defmodule MyServer do
|
||||
@unknown
|
||||
end
|
||||
warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it before access
|
||||
```
|
||||
|
||||
Attributes can also be read inside functions:
|
||||
|
||||
```elixir
|
||||
defmodule MyServer do
|
||||
@my_data 14
|
||||
def first_data, do: @my_data
|
||||
@my_data 13
|
||||
def second_data, do: @my_data
|
||||
end
|
||||
|
||||
MyServer.first_data #=> 14
|
||||
MyServer.second_data #=> 13
|
||||
```
|
||||
|
||||
> Do not add a newline between the attribute and its value, otherwise Elixir will assume you are reading the value, rather than setting it.
|
||||
|
||||
Functions may be called when defining a module attribute:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Status do
|
||||
@service URI.parse("https://example.com")
|
||||
def status(email) do
|
||||
SomeHttpClient.get(@service)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The function above will be called at compilation time and its *return value*, not the function call itself, is what will be substituted in for the attribute. So the above will effectively compile to this:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Status do
|
||||
def status(email) do
|
||||
SomeHttpClient.get(%URI{
|
||||
authority: "example.com",
|
||||
host: "example.com",
|
||||
port: 443,
|
||||
scheme: "https"
|
||||
})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This can be useful for pre-computing constant values, but it can also cause problems if you're expecting the function to be called at runtime. For example, if you are reading a value from a database or an environment variable inside an attribute, be aware that it will read that value only at compilation time. However, note you cannot invoke functions defined in the same module as part of the attribute itself, as those functions have not yet been defined.
|
||||
|
||||
Every time an attribute is read inside a function, Elixir takes a snapshot of its current value. Therefore if you read the same attribute multiple times inside multiple functions, you may end-up making multiple copies of it. That's usually not an issue, but if you are using functions to compute large module attributes, that can slow down compilation. The solution is to move the attribute to shared function. For example, instead of this:
|
||||
|
||||
```elixir
|
||||
def some_function, do: do_something_with(@example)
|
||||
def another_function, do: do_something_else_with(@example)
|
||||
```
|
||||
|
||||
Prefer this:
|
||||
|
||||
```elixir
|
||||
def some_function, do: do_something_with(example())
|
||||
def another_function, do: do_something_else_with(example())
|
||||
defp example, do: @example
|
||||
```
|
||||
|
||||
If `@example` is cheap to compute, it may be even better to skip the module attribute altogether, and compute its value inside the function.
|
||||
|
||||
### Accumulating attributes
|
||||
|
||||
Normally, repeating a module attribute will cause its value to be reassigned, but there are circumstances where you may want to [configure the module attribute](`Module.register_attribute/3`) so that its values are accumulated:
|
||||
|
||||
```elixir
|
||||
defmodule Foo do
|
||||
Module.register_attribute(__MODULE__, :param, accumulate: true)
|
||||
|
||||
@param :foo
|
||||
@param :bar
|
||||
# here @param == [:bar, :foo]
|
||||
end
|
||||
```
|
||||
|
||||
## As temporary storage
|
||||
|
||||
To see an example of using module attributes as storage, look no further than Elixir's unit test framework called `ExUnit`. ExUnit uses module attributes for multiple different purposes:
|
||||
|
||||
```elixir
|
||||
defmodule MyTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
@tag :external
|
||||
@tag os: :unix
|
||||
test "contacts external service" do
|
||||
# ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
In the example above, `ExUnit` stores the value of `async: true` in a module attribute to change how the module is compiled. Tags are also defined as `accumulate: true` attributes, and they store tags that can be used to setup and filter tests. For example, you can avoid running external tests on your machine because they are slow and dependent on other services, while they can still be enabled in your build system.
|
||||
|
||||
In order to understand the underlying code, we'd need macros, so we will revisit this pattern in the meta-programming guide and learn how to use module attributes as storage to allow developers to create Domain Specific Languages (DSLs).
|
||||
|
||||
In the next chapters, we'll explore structs and protocols before moving to exception handling and other constructs like sigils and comprehensions.
|
||||
@@ -0,0 +1,235 @@
|
||||
# Modules and functions
|
||||
|
||||
In Elixir we group several functions into modules. We've already used many different modules in the previous chapters, such as the `String` module:
|
||||
|
||||
```elixir
|
||||
iex> String.length("hello")
|
||||
5
|
||||
```
|
||||
|
||||
In order to create our own modules in Elixir, we use the `defmodule` macro. The first letter of the module must be in uppercase. We use the `def` macro to define functions in that module. The first letter of every function must be in lowercase (or underscore):
|
||||
|
||||
```elixir
|
||||
iex> defmodule Math do
|
||||
...> def sum(a, b) do
|
||||
...> a + b
|
||||
...> end
|
||||
...> end
|
||||
|
||||
iex> Math.sum(1, 2)
|
||||
3
|
||||
```
|
||||
|
||||
In this chapter we will define our own modules, with different levels of complexity. As our examples get longer in size, it can be tricky to type them all in the shell. It's about time for us to learn how to compile Elixir code and also how to run Elixir scripts.
|
||||
|
||||
## Compilation
|
||||
|
||||
Most of the time it is convenient to write modules into files so they can be compiled and reused. Let's assume we have a file named `math.ex` with the following contents:
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
def sum(a, b) do
|
||||
a + b
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This file can be compiled using `elixirc`:
|
||||
|
||||
```console
|
||||
$ elixirc math.ex
|
||||
```
|
||||
|
||||
This will generate a file named `Elixir.Math.beam` containing the bytecode for the defined module. If we start `iex` again, our module definition will be available (provided that `iex` is started in the same directory the bytecode file is in):
|
||||
|
||||
```elixir
|
||||
iex> Math.sum(1, 2)
|
||||
3
|
||||
```
|
||||
|
||||
Elixir projects are usually organized into three directories:
|
||||
|
||||
* `_build` - contains compilation artifacts
|
||||
* `lib` - contains Elixir code (usually `.ex` files)
|
||||
* `test` - contains tests (usually `.exs` files)
|
||||
|
||||
When working on actual projects, the build tool called `mix` will be responsible for compiling and setting up the proper paths for you. For learning and convenience purposes, Elixir also supports a scripting mode which is more flexible and does not generate any compiled artifacts.
|
||||
|
||||
## Scripting mode
|
||||
|
||||
In addition to the Elixir file extension `.ex`, Elixir also supports `.exs` files for scripting. Elixir treats both files exactly the same way, the only difference is in intention. `.ex` files are meant to be compiled while `.exs` files are used for scripting. This convention is followed by projects like `mix`.
|
||||
|
||||
For instance, we can create a file called `math.exs`:
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
def sum(a, b) do
|
||||
a + b
|
||||
end
|
||||
end
|
||||
|
||||
IO.puts Math.sum(1, 2)
|
||||
```
|
||||
|
||||
And execute it as:
|
||||
|
||||
```console
|
||||
$ elixir math.exs
|
||||
```
|
||||
|
||||
Because we used `elixir` instead of `elixirc`, the module was compiled and loaded into memory, but no `.beam` file was written to disk. In the following examples, we recommend you write your code into script files and execute them as shown above.
|
||||
|
||||
## Function definition
|
||||
|
||||
Inside a module, we can define functions with `def/2` and private functions with `defp/2`. A function defined with `def/2` can be invoked from other modules while a private function can only be invoked locally.
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
def sum(a, b) do
|
||||
do_sum(a, b)
|
||||
end
|
||||
|
||||
defp do_sum(a, b) do
|
||||
a + b
|
||||
end
|
||||
end
|
||||
|
||||
IO.puts Math.sum(1, 2) #=> 3
|
||||
IO.puts Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)
|
||||
```
|
||||
|
||||
Function declarations also support guards and multiple clauses. If a function has several clauses, Elixir will try each clause until it finds one that matches. Here is an implementation of a function that checks if the given number is zero or not:
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
def zero?(0) do
|
||||
true
|
||||
end
|
||||
|
||||
def zero?(x) when is_integer(x) do
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
IO.puts Math.zero?(0) #=> true
|
||||
IO.puts Math.zero?(1) #=> false
|
||||
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
|
||||
IO.puts Math.zero?(0.0) #=> ** (FunctionClauseError)
|
||||
```
|
||||
|
||||
The trailing question mark in `zero?` means that this function returns a boolean. To learn more about the naming conventions for modules, function names, variables and more in Elixir, see [Naming Conventions](../references/naming-conventions.md).
|
||||
|
||||
Giving an argument that does not match any of the clauses raises an error.
|
||||
|
||||
Similar to constructs like `if`, function definitions support both `do:` and `do`-block syntax, as [we learned in the previous chapter](keywords-and-maps.md#do-blocks-and-keywords). For example, we can edit `math.exs` to look like this:
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
def zero?(0), do: true
|
||||
def zero?(x) when is_integer(x), do: false
|
||||
end
|
||||
```
|
||||
|
||||
And it will provide the same behaviour. You may use `do:` for one-liners but always use `do`-blocks for functions spanning multiple lines. If you prefer to be consistent, you can use `do`-blocks throughout your codebase.
|
||||
|
||||
## Default arguments
|
||||
|
||||
Function definitions in Elixir also support default arguments:
|
||||
|
||||
```elixir
|
||||
defmodule Concat do
|
||||
def join(a, b, sep \\ " ") do
|
||||
a <> sep <> b
|
||||
end
|
||||
end
|
||||
|
||||
IO.puts Concat.join("Hello", "world") #=> Hello world
|
||||
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
|
||||
```
|
||||
|
||||
Any expression is allowed to serve as a default value, but it won't be evaluated during the function definition. Every time the function is invoked and any of its default values have to be used, the expression for that default value will be evaluated:
|
||||
|
||||
```elixir
|
||||
defmodule DefaultTest do
|
||||
def dowork(x \\ "hello") do
|
||||
x
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> DefaultTest.dowork()
|
||||
"hello"
|
||||
iex> DefaultTest.dowork(123)
|
||||
123
|
||||
iex> DefaultTest.dowork()
|
||||
"hello"
|
||||
```
|
||||
|
||||
If a function with default values has multiple clauses, it is required to create a function head (a function definition without a body) for declaring defaults:
|
||||
|
||||
```elixir
|
||||
defmodule Concat do
|
||||
# A function head declaring defaults
|
||||
def join(a, b \\ nil, sep \\ " ")
|
||||
|
||||
def join(a, b, _sep) when is_nil(b) do
|
||||
a
|
||||
end
|
||||
|
||||
def join(a, b, sep) do
|
||||
a <> sep <> b
|
||||
end
|
||||
end
|
||||
|
||||
IO.puts Concat.join("Hello", "world") #=> Hello world
|
||||
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
|
||||
IO.puts Concat.join("Hello") #=> Hello
|
||||
```
|
||||
|
||||
When a variable is not used by a function or a clause, we add a leading underscore (`_`) to its name to signal this intent. This rule is also covered in our [Naming Conventions](../references/naming-conventions.md#underscore-_foo) document.
|
||||
|
||||
When using default values, one must be careful to avoid overlapping function definitions. Consider the following example:
|
||||
|
||||
```elixir
|
||||
defmodule Concat do
|
||||
def join(a, b) do
|
||||
IO.puts "***First join"
|
||||
a <> b
|
||||
end
|
||||
|
||||
def join(a, b, sep \\ " ") do
|
||||
IO.puts "***Second join"
|
||||
a <> sep <> b
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Elixir will emit the following warning:
|
||||
|
||||
```text
|
||||
warning: this clause cannot match because a previous clause at line 2 always matches
|
||||
concat.ex:7: Concat
|
||||
```
|
||||
|
||||
The compiler is telling us that invoking the `join` function with two arguments will always choose the first definition of `join` whereas the second one will only be invoked when three arguments are passed:
|
||||
|
||||
```console
|
||||
$ iex concat.ex
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> Concat.join "Hello", "world"
|
||||
***First join
|
||||
"Helloworld"
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> Concat.join "Hello", "world", "_"
|
||||
***Second join
|
||||
"Hello_world"
|
||||
```
|
||||
|
||||
Removing the default argument in this case will fix the warning.
|
||||
|
||||
This finishes our short introduction to modules. In the next chapters, we will learn how to use function definitions for recursion and later on explore more functionality related to modules.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Optional syntax sheet
|
||||
|
||||
In this guide, we learned that the Elixir syntax allows developers to omit delimiters in a few occasions to make code more readable. For example, we learned that parentheses are optional:
|
||||
|
||||
```elixir
|
||||
iex> length([1, 2, 3]) == length [1, 2, 3]
|
||||
true
|
||||
```
|
||||
|
||||
and that `do`-`end` blocks are equivalent to keyword lists:
|
||||
|
||||
```elixir
|
||||
# do-end blocks
|
||||
iex> if true do
|
||||
...> :this
|
||||
...> else
|
||||
...> :that
|
||||
...> end
|
||||
:this
|
||||
|
||||
# keyword lists
|
||||
iex> if true, do: :this, else: :that
|
||||
:this
|
||||
```
|
||||
|
||||
Keyword lists use Elixir's regular notation for separating arguments, where we separate each key-value pair with commas, and each key is followed by `:`. In the `do`-blocks, we get rid of the colons, the commas, and separate each keyword by a newline. They are useful exactly because they remove the verbosity when writing blocks of code. Most of the time, we use the block syntax, but it is good to know they are equivalent.
|
||||
|
||||
Those conveniences, which we call here "optional syntax", allow the language syntax core to be small, without sacrificing the readability and expressiveness of your code. In this brief chapter, we will review the four rules provided by the language, using a short snippet as playground.
|
||||
|
||||
## Walk-through
|
||||
|
||||
Take the following code:
|
||||
|
||||
```elixir
|
||||
if variable? do
|
||||
Call.this()
|
||||
else
|
||||
Call.that()
|
||||
end
|
||||
```
|
||||
|
||||
Now let's remove the conveniences one by one:
|
||||
|
||||
1. `do`-`end` blocks are equivalent to keywords:
|
||||
|
||||
```elixir
|
||||
if variable?, do: Call.this(), else: Call.that()
|
||||
```
|
||||
|
||||
2. Keyword lists as last argument do not require square brackets, but let's add them:
|
||||
|
||||
```elixir
|
||||
if variable?, [do: Call.this(), else: Call.that()]
|
||||
```
|
||||
|
||||
3. Keyword lists are the same as lists of two-element tuples:
|
||||
|
||||
```elixir
|
||||
if variable?, [{:do, Call.this()}, {:else, Call.that()}]
|
||||
```
|
||||
|
||||
4. Finally, parentheses are optional on function calls, but let's add them:
|
||||
|
||||
```elixir
|
||||
if(variable?, [{:do, Call.this()}, {:else, Call.that()}])
|
||||
```
|
||||
|
||||
That's it! Those four rules outline the optional syntax available in Elixir.
|
||||
|
||||
To understand why these rules matter, we can briefly compare Elixir with many other programming languages. Most programming languages have several keywords for defining methods, functions, conditionals, loops, and so forth. Each of those keywords have their own syntax rules attached to them.
|
||||
|
||||
However, in Elixir, none of these language features require special "keywords", instead they all build from this small set of rules. The other benefit is that developers can also extend the language in a way that is consistent with the language itself, since the constructs for designing and extending the language are the same. We further explore this topic in [the "Meta-programming" guide](../meta-programming/quote-and-unquote.md).
|
||||
|
||||
At the end of the day, those rules are what enables us to write:
|
||||
|
||||
```elixir
|
||||
defmodule Math do
|
||||
def add(a, b) do
|
||||
a + b
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
instead of:
|
||||
|
||||
```elixir
|
||||
defmodule(Math, [
|
||||
{:do, def(add(a, b), [{:do, a + b}])}
|
||||
])
|
||||
```
|
||||
|
||||
Whenever you have any questions, this quick walk-through has you covered.
|
||||
|
||||
Finally, if you are concerned about when to apply these rules, it's worth noting that the Elixir formatter handles those concerns for you. Most Elixir developers use the `mix format` task to format their codebases according to a well-defined set of rules defined by the Elixir team and the community. For instance, `mix format` will always add parentheses to function calls unless explicitly configured not to do so. This helps to maintain consistency across all codebases within organizations and the wider community.
|
||||
@@ -0,0 +1,198 @@
|
||||
# Pattern matching
|
||||
|
||||
In this chapter, we will learn why the `=` operator in Elixir is called the match operator and how to use it to pattern match inside data structures. We will learn about the pin operator `^` used to access previously bound values.
|
||||
|
||||
## The match operator
|
||||
|
||||
We have used the `=` operator a couple times to assign variables in Elixir:
|
||||
|
||||
```elixir
|
||||
iex> x = 1
|
||||
1
|
||||
iex> x
|
||||
1
|
||||
```
|
||||
|
||||
In Elixir, the `=` operator is actually called *the match operator*. Let's see why:
|
||||
|
||||
```elixir
|
||||
iex> x = 1
|
||||
1
|
||||
iex> 1 = x
|
||||
1
|
||||
iex> 2 = x
|
||||
** (MatchError) no match of right hand side value: 1
|
||||
```
|
||||
|
||||
Notice that `1 = x` is a valid expression, and it matched because both the left and right side are equal to 1. When the sides do not match, a `MatchError` is raised.
|
||||
|
||||
A variable can only be assigned on the left side of `=`:
|
||||
|
||||
```elixir
|
||||
iex> 1 = unknown
|
||||
** (CompileError) iex:1: undefined variable "unknown"
|
||||
```
|
||||
|
||||
## Pattern matching
|
||||
|
||||
The match operator is not only used to match against simple values, but it is also useful for destructuring more complex data types. For example, we can pattern match on tuples:
|
||||
|
||||
```elixir
|
||||
iex> {a, b, c} = {:hello, "world", 42}
|
||||
{:hello, "world", 42}
|
||||
iex> a
|
||||
:hello
|
||||
iex> b
|
||||
"world"
|
||||
```
|
||||
|
||||
A pattern match error will occur if the sides can't be matched, for example if the tuples have different sizes:
|
||||
|
||||
```elixir
|
||||
iex> {a, b, c} = {:hello, "world"}
|
||||
** (MatchError) no match of right hand side value: {:hello, "world"}
|
||||
```
|
||||
|
||||
And also when comparing different types, for example if matching a tuple on the left side with a list on the right side:
|
||||
|
||||
```elixir
|
||||
iex> {a, b, c} = [:hello, "world", 42]
|
||||
** (MatchError) no match of right hand side value: [:hello, "world", 42]
|
||||
```
|
||||
|
||||
More interestingly, we can match on specific values. The example below asserts that the left side will only match the right side when the right side is a tuple that starts with the atom `:ok`:
|
||||
|
||||
```elixir
|
||||
iex> {:ok, result} = {:ok, 13}
|
||||
{:ok, 13}
|
||||
iex> result
|
||||
13
|
||||
|
||||
iex> {:ok, result} = {:error, :oops}
|
||||
** (MatchError) no match of right hand side value: {:error, :oops}
|
||||
```
|
||||
|
||||
We can pattern match on lists:
|
||||
|
||||
```elixir
|
||||
iex> [a, b, c] = [1, 2, 3]
|
||||
[1, 2, 3]
|
||||
iex> a
|
||||
1
|
||||
```
|
||||
|
||||
A list also supports matching on its own head and tail:
|
||||
|
||||
```elixir
|
||||
iex> [head | tail] = [1, 2, 3]
|
||||
[1, 2, 3]
|
||||
iex> head
|
||||
1
|
||||
iex> tail
|
||||
[2, 3]
|
||||
```
|
||||
|
||||
Similar to the `hd/1` and `tl/1` functions, we can't match an empty list with a head and tail pattern:
|
||||
|
||||
```elixir
|
||||
iex> [head | tail] = []
|
||||
** (MatchError) no match of right hand side value: []
|
||||
```
|
||||
|
||||
The `[head | tail]` format is not only used on pattern matching but also for prepending items to a list:
|
||||
|
||||
```elixir
|
||||
iex> list = [1, 2, 3]
|
||||
[1, 2, 3]
|
||||
iex> [0 | list]
|
||||
[0, 1, 2, 3]
|
||||
```
|
||||
|
||||
Pattern matching allows developers to easily destructure data types such as tuples and lists. As we will see in the following chapters, it is one of the foundations of recursion in Elixir and applies to other types as well, like maps and binaries.
|
||||
|
||||
## The pin operator
|
||||
|
||||
Variables in Elixir can be rebound:
|
||||
|
||||
```elixir
|
||||
iex> x = 1
|
||||
1
|
||||
iex> x = 2
|
||||
2
|
||||
```
|
||||
|
||||
However, there are times when we don't want variables to be rebound.
|
||||
|
||||
Use the pin operator `^` when you want to pattern match against a variable's *existing value* rather than rebinding the variable.
|
||||
|
||||
```elixir
|
||||
iex> x = 1
|
||||
1
|
||||
iex> ^x = 2
|
||||
** (MatchError) no match of right hand side value: 2
|
||||
```
|
||||
|
||||
Because we have pinned `x` when it was bound to the value of `1`, it is equivalent to the following:
|
||||
|
||||
```elixir
|
||||
iex> 1 = 2
|
||||
** (MatchError) no match of right hand side value: 2
|
||||
```
|
||||
|
||||
Notice that we even see the exact same error message.
|
||||
|
||||
We can use the pin operator inside other pattern matches, such as tuples or lists:
|
||||
|
||||
```elixir
|
||||
iex> x = 1
|
||||
1
|
||||
iex> [^x, 2, 3] = [1, 2, 3]
|
||||
[1, 2, 3]
|
||||
iex> {y, ^x} = {2, 1}
|
||||
{2, 1}
|
||||
iex> y
|
||||
2
|
||||
iex> {y, ^x} = {2, 2}
|
||||
** (MatchError) no match of right hand side value: {2, 2}
|
||||
```
|
||||
|
||||
Because `x` was bound to the value of `1` when it was pinned, this last example could have been written as:
|
||||
|
||||
```elixir
|
||||
iex> {y, 1} = {2, 2}
|
||||
** (MatchError) no match of right hand side value: {2, 2}
|
||||
```
|
||||
|
||||
If a variable is mentioned more than once in a pattern, all references must bind to the same value:
|
||||
|
||||
```elixir
|
||||
iex> {x, x} = {1, 1}
|
||||
{1, 1}
|
||||
iex> {x, x} = {1, 2}
|
||||
** (MatchError) no match of right hand side value: {1, 2}
|
||||
```
|
||||
|
||||
In some cases, you don't care about a particular value in a pattern. It is a common practice to bind those values to the underscore, `_`. For example, if only the head of the list matters to us, we can assign the tail to underscore:
|
||||
|
||||
```elixir
|
||||
iex> [head | _] = [1, 2, 3]
|
||||
[1, 2, 3]
|
||||
iex> head
|
||||
1
|
||||
```
|
||||
|
||||
The variable `_` is special in that it can never be read from. Trying to read from it gives a compile error:
|
||||
|
||||
```elixir
|
||||
iex> _
|
||||
** (CompileError) iex:1: invalid use of _. "_" represents a value to be ignored in a pattern and cannot be used in expressions
|
||||
```
|
||||
|
||||
Although pattern matching allows us to build powerful constructs, its usage is limited. For instance, you cannot make function calls on the left side of a match. The following example is invalid:
|
||||
|
||||
```elixir
|
||||
iex> length([1, [2], 3]) = 3
|
||||
** (CompileError) iex:1: cannot invoke remote function :erlang.length/1 inside match
|
||||
```
|
||||
|
||||
This finishes our introduction to pattern matching. As we will see in the next chapter, pattern matching is very common in many language constructs and they can be further augmented with guards.
|
||||
@@ -0,0 +1,235 @@
|
||||
# Processes
|
||||
|
||||
In Elixir, all code runs inside processes. Processes are isolated from each other, run concurrent to one another and communicate via message passing. Processes are not only the basis for concurrency in Elixir, but they also provide the means for building distributed and fault-tolerant programs.
|
||||
|
||||
Elixir's processes should not be confused with operating system processes. Processes in Elixir are extremely lightweight in terms of memory and CPU (even compared to threads as used in many other programming languages). Because of this, it is not uncommon to have tens or even hundreds of thousands of processes running simultaneously.
|
||||
|
||||
In this chapter, we will learn about the basic constructs for spawning new processes, as well as sending and receiving messages between processes.
|
||||
|
||||
## Spawning processes
|
||||
|
||||
The basic mechanism for spawning new processes is the auto-imported `spawn/1` function:
|
||||
|
||||
```elixir
|
||||
iex> spawn(fn -> 1 + 2 end)
|
||||
#PID<0.43.0>
|
||||
```
|
||||
|
||||
`spawn/1` takes a function which it will execute in another process.
|
||||
|
||||
Notice `spawn/1` returns a PID (process identifier). At this point, the process you spawned is very likely dead. The spawned process will execute the given function and exit after the function is done:
|
||||
|
||||
```elixir
|
||||
iex> pid = spawn(fn -> 1 + 2 end)
|
||||
#PID<0.44.0>
|
||||
iex> Process.alive?(pid)
|
||||
false
|
||||
```
|
||||
|
||||
> Note: you will likely get different process identifiers than the ones we are getting in this guide.
|
||||
|
||||
We can retrieve the PID of the current process by calling `self/0`:
|
||||
|
||||
```elixir
|
||||
iex> self()
|
||||
#PID<0.41.0>
|
||||
iex> Process.alive?(self())
|
||||
true
|
||||
```
|
||||
|
||||
Processes get much more interesting when we are able to send and receive messages.
|
||||
|
||||
## Sending and receiving messages
|
||||
|
||||
We can send messages to a process with `send/2` and receive them with `receive/1`:
|
||||
|
||||
```elixir
|
||||
iex> send(self(), {:hello, "world"})
|
||||
{:hello, "world"}
|
||||
iex> receive do
|
||||
...> {:hello, msg} -> msg
|
||||
...> {:world, _msg} -> "won't match"
|
||||
...> end
|
||||
"world"
|
||||
```
|
||||
|
||||
When a message is sent to a process, the message is stored in the process mailbox. The `receive/1` block goes through the current process mailbox searching for a message that matches any of the given patterns. `receive/1` supports guards and many clauses, such as `case/2`.
|
||||
|
||||
The process that sends the message does not block on `send/2`, it puts the message in the recipient's mailbox and continues. In particular, a process can send messages to itself.
|
||||
|
||||
If there is no message in the mailbox matching any of the patterns, the current process will wait until a matching message arrives. A timeout can also be specified:
|
||||
|
||||
```elixir
|
||||
iex> receive do
|
||||
...> {:hello, msg} -> msg
|
||||
...> after
|
||||
...> 1_000 -> "nothing after 1s"
|
||||
...> end
|
||||
"nothing after 1s"
|
||||
```
|
||||
|
||||
A timeout of 0 can be given when you already expect the message to be in the mailbox.
|
||||
|
||||
Let's put it all together and send messages between processes:
|
||||
|
||||
```elixir
|
||||
iex> parent = self()
|
||||
#PID<0.41.0>
|
||||
iex> spawn(fn -> send(parent, {:hello, self()}) end)
|
||||
#PID<0.48.0>
|
||||
iex> receive do
|
||||
...> {:hello, pid} -> "Got hello from #{inspect pid}"
|
||||
...> end
|
||||
"Got hello from #PID<0.48.0>"
|
||||
```
|
||||
|
||||
The `inspect/1` function is used to convert a data structure's internal representation into a string, typically for printing. Notice that when the `receive` block gets executed the sender process we have spawned may already be dead, as its only instruction was to send a message.
|
||||
|
||||
While in the shell, you may find the helper `flush/0` quite useful. It flushes and prints all the messages in the mailbox.
|
||||
|
||||
```elixir
|
||||
iex> send(self(), :hello)
|
||||
:hello
|
||||
iex> flush()
|
||||
:hello
|
||||
:ok
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
The majority of times we spawn processes in Elixir, we spawn them as linked processes. Before we show an example with `spawn_link/1`, let's see what happens when a process started with `spawn/1` fails:
|
||||
|
||||
```elixir
|
||||
iex> spawn(fn -> raise "oops" end)
|
||||
#PID<0.58.0>
|
||||
|
||||
[error] Process #PID<0.58.00> raised an exception
|
||||
** (RuntimeError) oops
|
||||
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
|
||||
```
|
||||
|
||||
It merely logged an error but the parent process is still running. That's because processes are isolated. If we want the failure in one process to propagate to another one, we should link them. This can be done with `spawn_link/1`:
|
||||
|
||||
```elixir
|
||||
iex> self()
|
||||
#PID<0.41.0>
|
||||
iex> spawn_link(fn -> raise "oops" end)
|
||||
|
||||
** (EXIT from #PID<0.41.0>) evaluator process exited with reason: an exception was raised:
|
||||
** (RuntimeError) oops
|
||||
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
|
||||
|
||||
[error] Process #PID<0.289.0> raised an exception
|
||||
** (RuntimeError) oops
|
||||
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
|
||||
```
|
||||
|
||||
Because processes are linked, we now see a message saying the parent process, which is the shell process, has received an EXIT signal from another process causing the shell to terminate. IEx detects this situation and starts a new shell session.
|
||||
|
||||
Linking can also be done manually by calling `Process.link/1`. We recommend that you take a look at the `Process` module for other functionality provided by processes.
|
||||
|
||||
Processes and links play an important role when building fault-tolerant systems. Elixir processes are isolated and don't share anything by default. Therefore, a failure in a process will never crash or corrupt the state of another process. Links, however, allow processes to establish a relationship in case of failure. We often link our processes to supervisors which will detect when a process dies and start a new process in its place.
|
||||
|
||||
While other languages would require us to catch/handle exceptions, in Elixir we are actually fine with letting processes fail because we expect supervisors to properly restart our systems. "Failing fast" (sometimes referred as "let it crash") is a common philosophy when writing Elixir software!
|
||||
|
||||
`spawn/1` and `spawn_link/1` are the basic primitives for creating processes in Elixir. Although we have used them exclusively so far, most of the time we are going to use abstractions that build on top of them. Let's see the most common one, called tasks.
|
||||
|
||||
## Tasks
|
||||
|
||||
Tasks build on top of the spawn functions to provide better error reports and introspection:
|
||||
|
||||
```elixir
|
||||
iex> Task.start(fn -> raise "oops" end)
|
||||
{:ok, #PID<0.55.0>}
|
||||
|
||||
15:22:33.046 [error] Task #PID<0.55.0> started from #PID<0.53.0> terminating
|
||||
** (RuntimeError) oops
|
||||
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
|
||||
(elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
|
||||
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
|
||||
Function: #Function<20.99386804/0 in :erl_eval.expr/5>
|
||||
Args: []
|
||||
```
|
||||
|
||||
Instead of `spawn/1` and `spawn_link/1`, we use `Task.start/1` and `Task.start_link/1` which return `{:ok, pid}` rather than just the PID. This is what enables tasks to be used in supervision trees. Furthermore, `Task` provides convenience functions, like `Task.async/1` and `Task.await/1`, and functionality to ease distribution.
|
||||
|
||||
We will explore tasks and other abstractions around processes in the ["Mix and OTP guide"](../mix-and-otp/introduction-to-mix.md).
|
||||
|
||||
## State
|
||||
|
||||
We haven't talked about state so far in this guide. If you are building an application that requires state, for example, to keep your application configuration, or you need to parse a file and keep it in memory, where would you store it?
|
||||
|
||||
Processes are the most common answer to this question. We can write processes that loop infinitely, maintain state, and send and receive messages. As an example, let's write a module that starts new processes that work as a key-value store in a file named `kv.exs`:
|
||||
|
||||
```elixir
|
||||
defmodule KV do
|
||||
def start_link do
|
||||
Task.start_link(fn -> loop(%{}) end)
|
||||
end
|
||||
|
||||
defp loop(map) do
|
||||
receive do
|
||||
{:get, key, caller} ->
|
||||
send(caller, Map.get(map, key))
|
||||
loop(map)
|
||||
{:put, key, value} ->
|
||||
loop(Map.put(map, key, value))
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Note that the `start_link` function starts a new process that runs the `loop/1` function, starting with an empty map. The `loop/1` (private) function then waits for messages and performs the appropriate action for each message. We made `loop/1` private by using `defp` instead of `def`. In the case of a `:get` message, it sends a message back to the caller and calls `loop/1` again, to wait for a new message. While the `:put` message actually invokes `loop/1` with a new version of the map, with the given `key` and `value` stored.
|
||||
|
||||
Let's give it a try by running `iex kv.exs`:
|
||||
|
||||
```elixir
|
||||
iex> {:ok, pid} = KV.start_link()
|
||||
{:ok, #PID<0.62.0>}
|
||||
iex> send(pid, {:get, :hello, self()})
|
||||
{:get, :hello, #PID<0.41.0>}
|
||||
iex> flush()
|
||||
nil
|
||||
:ok
|
||||
```
|
||||
|
||||
At first, the process map has no keys, so sending a `:get` message and then flushing the current process inbox returns `nil`. Let's send a `:put` message and try it again:
|
||||
|
||||
```elixir
|
||||
iex> send(pid, {:put, :hello, :world})
|
||||
{:put, :hello, :world}
|
||||
iex> send(pid, {:get, :hello, self()})
|
||||
{:get, :hello, #PID<0.41.0>}
|
||||
iex> flush()
|
||||
:world
|
||||
:ok
|
||||
```
|
||||
|
||||
Notice how the process is keeping a state and we can get and update this state by sending the process messages. In fact, any process that knows the `pid` above will be able to send it messages and manipulate the state.
|
||||
|
||||
It is also possible to register the `pid`, giving it a name, and allowing everyone that knows the name to send it messages:
|
||||
|
||||
```elixir
|
||||
iex> Process.register(pid, :kv)
|
||||
true
|
||||
iex> send(:kv, {:get, :hello, self()})
|
||||
{:get, :hello, #PID<0.41.0>}
|
||||
iex> flush()
|
||||
:world
|
||||
:ok
|
||||
```
|
||||
|
||||
Using processes to maintain state and name registration are very common patterns in Elixir applications. However, most of the time, we won't implement those patterns manually as above, but by using one of the many abstractions that ship with Elixir. For example, Elixir provides `Agent`s, which are simple abstractions around state. Our code above could be directly written as:
|
||||
|
||||
```elixir
|
||||
iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
|
||||
{:ok, #PID<0.72.0>}
|
||||
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
|
||||
:ok
|
||||
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
|
||||
:world
|
||||
```
|
||||
|
||||
A `:name` option could also be given to `Agent.start_link/2` and it would be automatically registered. Besides agents, Elixir provides an API for building generic servers (called `GenServer`), registries, and more, all powered by processes underneath. Those, along with supervision trees, will be explored with more detail in the ["Mix and OTP guide"](../mix-and-otp/introduction-to-mix.md), which will build a complete Elixir application from start to finish.
|
||||
|
||||
For now, let's move on and explore the world of I/O in Elixir.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user