Compare commits

..
101 Commits
Author SHA1 Message Date
José Valim 87710cd495 Release v1.3.4 2016-10-09 16:28:40 +02:00
Magnus Lång 6e2c29c9d4 Fix Dialyzer warnings on opaque protocol calls (#5286)
Prior to this change, calling a protocol function with an opaque type
would yield a warning, as Dialyzer concludes that the impl_for/1
function can't handle opaque arguments, since all clauses would
destructure their arguments in some way.

By adding a catch-all clause that does not destructure its argument,
Dialyzer no longer draws this conclusion, and the warnings go away.

As noted in the protocol.ex comment, this is technically a hack as it
relies on Dialyzer not being smart enough. However, I would not expect
it to break soon, if ever.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-10-04 14:17:47 +02:00
James Fish f7b32487c0 Flush error_logger before capturing (#5280)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-10-01 10:39:22 +02:00
José Valim eedc9bc757 Do not build matches if variables are the same
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-09-27 18:25:04 +02:00
José Valim f053df848f Move raise checks to runtime callback, closes #5257
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-09-26 20:36:22 +02:00
José Valim 0caa75760b Release v1.3.3 2016-09-17 17:47:40 +02:00
José Valim 87da507c24 Use NUL instead of /dev/null on Windows 2016-09-07 03:45:31 +02:00
José Valim f73e6f8711 Also store external resources that are not part of the cwd
Solves https://github.com/phoenixframework/phoenix_live_reload/issues/37
2016-09-05 20:38:22 +02:00
Jeff Gulbronson 3610d59214 Make ExUnit server timeout configurable
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-24 18:20:10 +02:00
José Valim 6243fb2755 Fix failing backported test 2016-08-20 18:00:04 +02:00
José Valim 7c5abe35e6 Fix order dependency in logger suite
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-20 06:09:21 +02:00
José Valim fc8314508f Allow git migration to and from sparse
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-20 06:07:20 +02:00
Justin Schneck 091ce84e18 add git sparse checkout (#5063)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-20 06:07:16 +02:00
José Valim bbce69b58e Allow logger to use ansi_color from metadata
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-20 04:15:14 +02:00
pareeohnos 56779fc25b Resolves issue with autocompletion on structs not working in IEx (#5133)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-12 16:04:28 +02:00
Qqwy 13b432c48d Enhancement: negative integer support in DateTime.from_unix (#5128)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-12 16:04:20 +02:00
Milton Mazzarri effb685854 Do not re-compile a compiled pattern for String.split/3 (#5127)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-11 09:47:44 +02:00
Eric Entin a51c1beb77 Do not consider remote typespecs as a compile-time dependency (#5093)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-04 20:36:02 +02:00
José Valim 8a226218e0 Always include the compiled file source in manifests
See https://github.com/plataformatec/nimble_csv/issues/4.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-04 20:35:53 +02:00
Tobias Pfeiffer a9559ef045 Document that the :consolidate_protocols option is true by default (#5091)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-04 20:35:26 +02:00
Eric Entin cb4310111b Do not wait for self in parallel compiler (#5086)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-04 20:34:57 +02:00
Michał Muskała 46cab56dfd Mark struct update syntax as generated (#5067)
With changes to OTP 19, dialyzer started emitting warnings for
the struct update syntax where variable could only be that struct.
For example:

    def foo(%Foo{} = struct), do: %Foo{struct | bar: :baz}

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-08-04 20:33:48 +02:00
Christopher 7aebe55236 Fix String.split when parts and trim is used (#5077)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-30 10:55:28 +02:00
Eric Entin 8aab3ce628 mix xref: loadpaths before compile (#5050)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-24 10:24:04 +02:00
Eric Meadows-Jönsson 59d6c803df Remove accidental file
Signed-off-by: Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>
2016-07-16 23:26:12 +02:00
Eric Meadows-Jönsson 72eb5c73a5 Do not unload deps for MIX_NO_DEPS=1
Signed-off-by: Eric Meadows-Jönsson <eric.meadows.jonsson@gmail.com>
2016-07-16 23:21:18 +02:00
José Valim 982aba3519 Always skip deps with MIX_NO_DEPS
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-16 00:03:15 +02:00
José Valim 56f1037973 Release v1.3.2 2016-07-15 18:49:26 +02:00
Alex e5fcef3832 Make sure to return nil if enumerable halts
Fix issue #5004

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-15 10:55:47 +02:00
José Valim 9081a7aad2 Update VERSION 2016-07-13 00:15:40 +02:00
Eric Meadows-Jönsson 328f581ef5 Add MIX_NO_DEPS env var for disabling dep loading
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-12 17:51:37 +02:00
Eksperimental aa695875fe Standardize mix escript task's shortdoc (#4984)
Correct verb tense, and remove trailing period.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-12 17:51:37 +02:00
Aleksei Magusev aa039ce9f9 Fix edit script for maps when there are no joint elements
Closes #4977.
2016-07-11 01:19:30 +02:00
Eric Entin 2fdf96e747 The else clause in with supports guards (#4960)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-07 20:26:52 +02:00
José Valim 494bded12a deps.check -> deps.loadpaths
deps.loadpaths was public API that Nerves, in
particular, depended on. This commit brings
back the deps.loadpaths task, replacing the
incorrect deps.check that should not be required
by Nerves.
2016-07-05 21:07:11 +02:00
José Valim 2b0fc529ee Improve error messages for :only conflict
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-04 20:58:01 +02:00
James Fish aaa775a3bf Fix @macrocallback spec translation
Signed-off-by: James Fish <james@fishcakez.com>
2016-07-04 18:21:25 +01:00
Andrea Leopardi 6647b00fcf Fix the spec for OptionParser.parse_head!/2
Signed-off-by: Andrea Leopardi <an.leopardi@gmail.com>
2016-07-04 00:04:49 +02:00
Andrea Leopardi d149c4c1e0 Polish and fix docs for OptionParser (#4924)
I also

* fixed some style in just a couple of lines of code
* renamed a couple of variables for clarity

Signed-off-by: Andrea Leopardi <an.leopardi@gmail.com>
2016-07-04 00:03:45 +02:00
Justin Schneck 58810bd33c add filter option to deps.unlock (#4932)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-03 22:08:09 +02:00
sunboshan 050cb45664 Fix typespec for Kernel.pop_in/2 (#4931)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-03 22:06:52 +02:00
Eric Entin 7f59aeeb6f mix test --listen-on-stdin will no longer exit on failure (#4930)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-03 19:12:53 +02:00
José Valim e2abd35b3c Ensure missing protocol dependencies are discarded, closes #4842
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-03 00:01:52 +02:00
Aleksei Magusev cc35c8fc22 Actually hide to_char_list/1 2016-07-02 00:34:33 +03:00
Eric Entin 2718631eb6 mix test --listen-on-stdin (#4921)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-07-01 21:02:04 +02:00
José Valim 9d9aa8bff8 Disable --warnings-as-errors for deps
See #4913
2016-06-30 22:26:35 +01:00
Eric Meadows-Jönsson 65aee00809 Add IS_DEP=1 env var when compiling make deps
When IS_DEP=1 erlang.mk disables warnings as errors.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 22:22:58 +01:00
Eric Meadows-Jönsson 9f028834d7 Reenable dialyzer reaise test
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 22:18:46 +01:00
Eric Meadows-Jönsson 44ef53ec2b Allow custom fields in Exception.t typespec
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 22:18:39 +01:00
Eric Meadows-Jönsson d1290d62db Fix struct/0 typespec
Closes #4915.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 22:18:32 +01:00
Mike Stok 301aa6eddc keep tokenizer's column counts in sync for numbers with _ characters
For providing better feedback in credo and tools like it we should be
able to map a token back to its original source.  This makes sure that
`_` characters in numbers are properly accounted for so that they stay
in sync after we've encountered something like 123_456_789.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 10:31:13 +01:00
José Valim 098b2d7cfd Ensure Access.at/1 properly pop list elements, closes #4911
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-30 10:31:10 +01:00
José Valim d92b7a331b Traverse dependnecies when consolidate umbrella protocols, closes #4842
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-29 01:21:18 +01:00
José Valim 8930ea7598 Do not attempt to load modules that have not been required, closes #4900 2016-06-29 01:20:45 +01:00
José Valim 01a1acd27b Release v1.3.1 2016-06-28 10:52:54 +01:00
José Valim 127691d4ad Ensure structs can be expanded in dynamic module names, closes #4894
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-28 10:42:34 +01:00
José Valim e848e87f52 Do not load modules for xref purposes, instead use BEAM info
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-28 01:00:26 +02:00
Daniel Perez bcadd96e31 Revert mix do to enforce space after comma (#4893)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-27 22:05:52 +02:00
Eric Entin 99a0bfa074 Skip/fix tests on OTP 19 (#4855)
* Callback-related tests use beam AST to find callbacks

* Fix invalid function call warning on OTP 19

* Skip dialyzer-related tests on OTP 19

* Fix IEx h helper for OTP 19

* Improve IEx h helper test for multi-arity callbacks

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-27 00:02:43 +02:00
Andrea Leopardi 5d00232633 Fix an assertion in Kernel.TypespecTest
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-26 19:34:50 +02:00
Andrea Leopardi c319bc450e Support module attributes in remote types (#4891)
For example, @for.t in protocols or similar use cases.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-26 19:34:46 +02:00
José Valim a9ed654ae2 Update CHANGELOG 2016-06-25 01:09:21 +02:00
Aleksei Magusev 6dacf53268 Fix Kernel.LexicalTracker not to discard alias directives on import
Closes #4871.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 01:05:46 +02:00
Aleksei Magusev 5e22b12272 Validate application properties before altering them
Closes #4873.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 01:05:26 +02:00
José Valim d4501893c7 Add import_file_if_available, closes #4878
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 00:48:02 +02:00
Benjamin Falk 744859ee42 Generate error for no pid given to Genserver.reply (#4876)
Prior to this change it is possible to send any two-pair tuple to
`GenServer.reply/2` and it will happily return `:ok`, catching any
errors.  This leads to some hard to track-down bugs if you pass
it the wrong term.

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 00:40:56 +02:00
Henrik Nyh 4596baab3f Add IEx.Helpers.import_if_available/2 (#4877)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-25 00:40:46 +02:00
Eksperimental c065041263 Add missing backquote in Module docs (#4866)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-23 23:44:50 +02:00
Daniel Perez 95bff23ede Fix h helper for binary operators, closes #4859 (#4860)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-23 10:11:06 +02:00
Marcus Gartner 0bc04195f2 Clarify Enum.group_by/3 deprecation message (#4852)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-22 19:02:04 +02:00
José Valim d195b75aa3 deps.check does not check archives 2016-06-22 10:54:28 +02:00
José Valim dc06a18287 Reenable deps.check on loadpaths dry-run
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-22 10:48:52 +02:00
José Valim 54c0574be2 Explicitly announce structs
Before this patch, we could announce a struct too early,
only when __struct__/0 was defined (and not __struct__/1)
leading to deadlocks in the compiler. We fixed the bug and
made struct announcements to avoid future bugs.

Closes #4844

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-22 10:05:13 +02:00
Daniel Perez 8d50e1fa95 Improve with/else error message on invalid form (#4847)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-22 09:33:28 +02:00
José Valim e70d9723eb Check for proper Makefile when compiling on Windows, closes #4841 2016-06-21 22:17:35 +02:00
José Valim 3ce029615d Do not expect stacktraces to be always present, closes #4840
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-21 20:27:01 +02:00
José Valim 416bf41a03 Release v1.3.0 2016-06-21 12:25:14 +02:00
José Valim c758c5e401 Update docs for c/1 and c/2 2016-06-21 12:14:04 +02:00
Aleksei Magusev 7b4576f0fc Consider variables are on different lines in assert_receive 2016-06-20 23:34:55 +02:00
José Valim 9192907e68 Do not leak external variables used in guards in assert_receive 2016-06-20 14:45:30 +02:00
José Valim 5533cc4665 Expose schedulers_online and otp_release information 2016-06-16 23:51:55 +02:00
Eric Entin 873dbc16ff more mix xref doc improvements (#4818)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 21:44:21 +02:00
José Valim 84c91ef914 Improve mix xref docs
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 21:08:05 +02:00
Eric Entin a977afcd6c mix xref graph (#4811)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 20:58:29 +02:00
Aleksei Magusev 8ed30ad926 Do not use deprecated String.strip
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 20:58:20 +02:00
Krzysztof Wende 8496d52df5 yecc instead of yeec (#4813)
Typo. should be `yecc` instead of `yeec` in the documentation

Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-15 20:58:15 +02:00
José Valim 977bb418a1 Ensure variables in function clauses are not reused, closes #4815 2016-06-15 16:14:37 +02:00
José Valim 735296a4ab Remove elixir_counter in favor of unique_integer 2016-06-15 15:25:04 +02:00
José Valim e34dde79ea Ensure entries are sorted to avoid non-determinism in compilation, closes #4814 2016-06-15 15:13:31 +02:00
José Valim 1eb11374c3 Update CHANGELOG 2016-06-12 23:53:07 +02:00
Eric Entin 9c2ec6a260 mix xref command-style (#4808)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-12 23:52:25 +02:00
Eric Entin 92cf02321b Make sure struct expansion results in a remote dispatch (#4807)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-12 23:52:09 +02:00
Aleksei Magusev 2b5e69aa05 Make sure with/1 does not leak variables to else (#4804)
Conflicts:
	lib/elixir/src/elixir_scope.erl
2016-06-12 21:39:20 +02:00
Aleksei Magusev a56606d5d6 Do not silence unused variable warnings in assert_receive
Closes #4789. It's not needed anymore after e190474955 commit.
2016-06-12 21:39:20 +02:00
José Valim b2329d11a8 Use explicit call for deps() 2016-06-12 18:29:04 +02:00
Felipe Seré 88b3e864d5 Clean deps when deps.compile is run with --force (#4780)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-12 10:00:24 +02:00
Josh Price efaca6c3e9 More informative message for DOT tree generation (#4791)
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-12 10:00:19 +02:00
José Valim 67aabe0bac Calculate beam files only when writing manifest data
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-11 15:38:24 +02:00
José Valim 8506cc68bf Ensure conflict with umbrella children are shown, closes #4792
Signed-off-by: José Valim <jose.valim@plataformatec.com.br>
2016-06-11 15:38:24 +02:00
Myron Marston a1bbc1bc20 Document change in behavior of Enum.group_by ordering. (#4794)
On 1.2, here's how `group_by` behaved:

    iex(1)> 1..10 |> Enum.group_by(fn i -> i < 5 end)
    %{false: [10, 9, 8, 7, 6, 5], true: [4, 3, 2, 1]}

On 1.3, here's how it behaves:

    iex(1)> 1..10 |> Enum.group_by(fn i -> i < 5 end)
    %{false: [5, 6, 7, 8, 9, 10], true: [1, 2, 3, 4]}
2016-06-09 20:40:11 +02:00
José Valim 18dfca21f0 Update doc references 2016-06-08 21:14:15 +02:00
455 changed files with 23003 additions and 46466 deletions
-9
View File
@@ -1,9 +0,0 @@
version: 1-{branch}+{build}
build_script:
- cmd: C:\MinGW\msys\1.0\bin\make
- cmd: rmdir /s /q .git
before_test:
- cmd: set PATH=%PATH%;C:\Program Files\erl8.3\erts-8.3\bin
test_script:
- cmd: C:\MinGW\msys\1.0\bin\make --keep-going test_windows
+8 -5
View File
@@ -1,14 +1,17 @@
/doc/
/lib/*/ebin/
/lib/*/_build/
/lib/*/tmp/
/doc
/ebin
/lib/*/ebin/*
/lib/*/tmp
/lib/elixir/src/*_lexer.erl
/lib/elixir/src/*_parser.erl
/lib/elixir/src/elixir.app.src
/lib/elixir/test/ebin/
/lib/elixir/test/ebin
/man/elixir.1
/man/iex.1
/rel/elixir
/Docs-v*.zip
/Precompiled-v*.zip
/.eunit
/.release
.elixir.plt
erl_crash.dump
+6 -30
View File
@@ -1,38 +1,14 @@
language: erlang
otp_release:
- 18.0
sudo: false
os: linux
otp_release: 18.0
matrix:
include:
- os: linux
otp_release: 18.1
- os: linux
otp_release: 18.2
- os: linux
otp_release: 18.3
- os: linux
otp_release: 19.0
- os: linux
otp_release: 19.1
- os: linux
otp_release: 19.2
- os: linux
otp_release: 19.3
- os: linux
otp_release: 20.0
env:
- ELIXIR_ASSERT_TIMEOUT=2000
script:
- make compile
- rm -rf .git
- make test
- dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
script: "make compile && rm -rf .git && make test"
notifications:
irc: "irc.freenode.org#elixir-lang"
recipients:
- jose.valim@plataformatec.com.br
- eric.meadows.jonsson@gmail.com
+424 -280
View File
@@ -1,375 +1,519 @@
# Changelog for Elixir v1.5
# Changelog for Elixir v1.3
Elixir v1.5 brings new features, enhancements and bug fixes to Elixir. It is the first release to leverage features added as part of Erlang/OTP 20. It is also the last release that supports Erlang/OTP 18.
Elixir v1.3 brings many improvements to the language, the compiler and its tooling, specially Mix (Elixir's build tool) and ExUnit (Elixir's test framework).
## UTF-8 atoms, function names and variables
## Language improvements
Elixir v1.5 supports non-quoted atoms and variables to be in UTF-8 when using Erlang/OTP 20+. For example:
The language has been improved semantically and includes new types and APIs. Let's see the three major features.
test "こんにちは世界" do
assert :こんにちは世界
### Deprecation of imperative assignment
Elixir will now warn if constructs like `if`, `case` and friends assign to a variable that is accessed in an outer scope. As an example, imagine a function called `format` that receives a message and some options and it must return a path alongside the message:
```elixir
def format(message, opts) do
path =
if (file = opts[:file]) && (line = opts[:line]) do
relative = Path.relative_to_cwd(file)
message = Exception.format_file_line(relative, line) <> " " <> message
relative
end
Or:
{path, message}
end
```
saudação = "Bom dia!"
The `if` block above is implicitly changing the value in `message`. Now imagine we want to move the `if` block to its own function to clean up the implementation:
Elixir follows the recommendations in [Unicode Annex #31](http://unicode.org/reports/tr31/) to make the language more accessible to other languages and communities. Identifiers must still be a sequence of letters, followed by digits and combining marks. This means symbols, such as mathematical notations and emoji, are not allowed identifiers.
```elixir
def format(message, opts) do
path = with_file_and_line(message, opts)
{path, message}
end
For a complete reference on Elixir syntax, see the [Syntax Reference](https://hexdocs.pm/elixir/syntax-reference.html). For technical details on Unicode support, see [Unicode Syntax](https://hexdocs.pm/elixir/unicode-syntax.html).
defp with_file_and_line(message, opts) do
if (file = opts[:file]) && (line = opts[:line]) do
relative = Path.relative_to_cwd(file)
message = Exception.format_file_line(relative, line) <> " " <> message
relative
end
end
```
## IEx improvements
The refactored version is broken because the `if` block was actually returning two values, the relative path *and* the new message. Elixir v1.3 will warn on such cases, forcing both variables to be explicitly returned from `if`, `case` and other constructs. Furthermore, this change gives us the opportunity to unify the language scoping rules in future releases.
IEx got many improvements. The autocompletion system is now capable of autocompleting variables and user imports. New helpers have also been added:
### Calendar types and sigils
* `exports/1` lists all exports (functions and macros) in a given module
* `open/1` opens up the source of a module or function directly in your editor. For example, `open MyApp.Module`
* `runtime_info/0` prints general information about the running system, such as number of cores, runtime version, allocation of memory in the VM and more
Elixir v1.3 introduces the `Calendar` module as well as 4 new calendar types:
IEx also features a breakpoint system for code debugging. The following functions have been added to aid debugging:
* `Date` - used to store dates (year, month, day) in a given calendar
* `Time` - used to store time (hour, minute, second, microseconds)
* `NaiveDateTime` - used to store datetimes without a timezone (year, month, day, hour, minute, second, microseconds) in a given calendar. It is called naïve because without a timezone, the datetime may not actually exist. For example, when there are daylight savings changes, a whole hour may not exist (when the clock moves forward) or a particular instant may happen twice (when the clock moves backwards)
* `DateTime` - used to store datetimes with timezone (year, month, day, hour, minute, second, microsecond and time zone, with abbreviation, UTC and standard offset)
* `break!/2` - sets up a breakpoint for a given `Mod.fun/arity`
* `break!/4` - sets up a breakpoint for the given module, function, arity
* `breaks/0` - prints all breakpoints and their ids
* `continue/0` - continues until the next breakpoint in the same process
* `open/0` - opens editor on the current breakpoint
* `remove_breaks/0` - removes all breakpoints in all modules
* `remove_breaks/1` - removes all breakpoints in a given module
* `reset_break/1` - sets the number of stops on the given id to zero
* `reset_break/3` - sets the number of stops on the given module, function, arity to zer
* `respawn/0` - starts a new shell (breakpoints will ask for permission once more)
* `whereami/1` - shows the current location
The current Calendar modules and its types is to provide a base for interoperatibility in the ecosystem instead of full-featured datetime API. This release includes basic functionality for building new types and converting them from and back strings.
## Exception.blame
Elixir v1.3 also introduces 3 new sigils related to the types above:
`Exception.blame/3` is a new function in Elixir that is capable of attaching debug information to certain exceptions. Currently this is used to augment `FunctionClauseError`s with a summary of all clauses and which parts of clause match and which ones didn't. For example:
* `~D[2016-05-29]` - builds a new date
* `~T[08:00:00]` and `~T[08:00:00.285]` - builds a new time (with different precisions)
* `~N[2016-05-29 08:00:00]` - builds a naive date time
iex> Access.fetch(:foo, :bar)
** (FunctionClauseError) no function clause matching in Access.fetch/2
### Access selectors
The following arguments were given to Access.fetch/2:
This release introduces new accessors to make it simpler for developers to traverse nested data structures, traversing and updating data in different ways. For instance, given a user with a list of languages, here is how to deeply traverse the map and convert all language names to uppercase:
# 1
:foo
```iex
iex> user = %{name: "john",
...> languages: [%{name: "elixir", type: :functional},
...> %{name: "c", type: :procedural}]}
iex> update_in user, [:languages, Access.all(), :name], &String.upcase/1
%{name: "john",
languages: [%{name: "ELIXIR", type: :functional},
%{name: "C", type: :procedural}]}
```
# 2
:bar
You can see the new accessors in the `Access` module.
Attempted function clauses (showing 5 out of 5):
## Mix
def fetch(-%struct{} = container-, key)
def fetch(map, key) when -is_map(map)-
def fetch(list, key) when -is_list(list)- and is_atom(key)
def fetch(list, key) when -is_list(list)-
def fetch(-nil-, _key)
Mix includes new tasks to improve your everyday workflow. Some of those tasks relies on many compiler improvements to know more about your code, providing static analysis to find possible bugs in your code and faster compilation cycles.
(elixir) lib/access.ex:261: Access.fetch/2
### Compiling n files
In the example above, an argument that did not match or guard that did not evaluate to true are shown between `-`. If the terminal supports ANSI coloring, they are wrapped in red instead of the `-` character.
Mix no longer announces every file it compiles. Instead it outputs how many files there is to compile per compilers. Here is the output for a project like [`gettext`](https://github.com/elixir-lang/gettext):
Since blaming an exception can be expensive, `Exception.blame/3` must be used exclusively in debugging situations. It is not advised to apply it to production components such as a Logger. This feature has been integrated into the compiler, the command line, ExUnit and IEx.
```
Compiling 1 file (.yrl)
Compiling 1 file (.erl)
Compiling 19 files (.ex)
Generated gettext app
```
This feature also requires Erlang/OTP 20+.
In case a file is taking too long to compile, Mix will announce such, for example:
## Streamlined child specs
```
Compiling lib/gettext.ex (it's taking more than 5s)
```
Elixir v1.5 streamlines how supervisors are defined and used in Elixir. Elixir now allows child specifications, which specify how a child process is supervised, to be defined in modules. In previous versions, a project using Phoenix would write:
The goal of these changes is to put an increased focus on the "warnings" emitted by the compiler.
import Supervisor.Spec
In any case, the previous behaviour can be brought back with the `--verbose` flag and the compilation threshold for files that are taking long can be set via the `--long-compilation-threshold` option.
children = [
supervisor(MyApp.Repo, []),
supervisor(MyApp.Endpoint, [])
]
### mix xref
Supervisor.start_link(children, strategy: :one_for_one)
Speaking about warnings, Mix v1.3 includes a new task called `xref` that performs cross reference checks in your code. One of such checks is the ability to find calls to modules and functions that do not exist. For example, if in your library code you call `ThisModuleDoesNotExist.foo(1, 2, 3)`, `mix xref unreachable` will be able to find such code and let you know about it.
In Elixir v1.5, one might do:
Since such checks can discover possible bugs in your codebase, a new compiler called `xref` has been added to `Mix.compilers/0`, so they run by default every time you compile your code.
children = [
MyApp.Repo,
MyApp.Endpoint
]
We have included other modes in `xref`, such as `mix xref callers Foo`, to find all places in your code that a function from the module `Foo` is called. We hope other tools and text editors can leverage such features to provide useful functionality for their users.
Supervisor.start_link(children, strategy: :one_for_one)
### Better dependency tracking
The above works by calling the `child_spec/1` function on the given modules.
Besides `xref`, Elixir v1.3 provides better module tracking generally. For example, in previous versions, if you changed a `:path` dependency, Elixir would always fully recompile the current project. In this release, we have improved the tracking algorithms such that, if you change a `:path` dependency, only the files that depend on such dependency are recompiled.
This new approach allows `MyApp.Repo` and `MyApp.Endpoint` to control how they run under a supervisor. This reduces the chances of mistakes being made, such as starting an Ecto repository as a worker or forgetting to declare that tasks are temporary in a supervision tree.
Such improvements do not only make compilation faster but they also make working with umbrella applications much more productive. Previously, changing a sibling application triggered a full project recompilation, now Elixir can track between sibling applications and recompile only what is needed.
If it is necessary to configure any of the children, such can be done by passing a tuple instead of an atom:
### mix app.tree and deps.tree
children = [
{MyApp.Repo, url: "ecto://localhost:4567/my_dev"},
MyApp.Endpoint
]
Mix also includes both `mix app.tree` and `mix deps.tree`. The first will list all applications your current project needs to start in order to boot (i.e. the ones listed in `application/0` in your `mix.exs`) while the second will lists all of your dependencies and so on recursively.
The modules `Agent`, `Registry`, `Task`, and `Task.Supervisor` have been updated to include a `child_spec/1` function, allowing them to be used directly in a supervision tree similar to the examples above. `use Agent`, `use GenServer`, `use Supervisor`, and `use Task` have also been updated to automatically define an overridable `child_spec/1` function.
Here is a quick example from [Plug](https://github.com/elixir-lang/plug):
Finally, child specifications are now provided as maps (data-structures) instead of the previous `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` APIs. This behaviour also aligns with how supervisors are configured in Erlang/OTP 18+. See the updated `Supervisor` docs for more information, as well as the new `Supervisor.init/2` and `Supervisor.child_spec/2` functions.
```elixir
$ mix app.tree
plug
├── elixir
├── crypto
├── logger
│ └── elixir
└── mime
└── elixir
```
## @impl
### mix escript.install
This release also allows developers to mark which functions in a given module are an implementation of a callback. For example, when using the [Plug](https://github.com/elixir-lang/plug) project, one needs to implement both `init/1` and `call/2` when writing a Plug:
Mix also includes `mix escript.install` and `mix escript.uninstall` tasks for managing escripts. The tasks was designed in a way to mimic the existing `mix archive` functionality except that:
defmodule MyApp do
@behaviour Plug
* Archives must be used sparingly because every new archive installed affects Mix performance, as every new archive is loaded when Mix boots. Escripts solve this by being managed apart from your Elixir/Mix installed
* Archives depends on the current Elixir version. Therefore, updating your Elixir version may break an archive. Fortunately, escripts include Elixir inside themselves, and therefore do not depend on your Elixir system version
def init(_opts) do
opts
end
Escripts will be installed at `~/.mix/escripts` which must be added to your [`PATH` environment variable](https://en.wikipedia.org/wiki/PATH_(variable)).
def call(conn, _opts) do
Plug.Conn.send_resp(conn, 200, "hello world")
end
### Option parser integration
Elixir v1.3 includes improvements to the option parser, including `OptionParser.parse!/2` and `OptionParser.parse_head!/2` functions that will raise in case of invalid or unknown switches. Mix builds on top of this functionality to provide automatic error reporting solving a common complaint where invalid options were not reported by Mix tasks.
For example, invoking `mix test --unknown` in earlier Elixir versions would silently discard the `--unknown` option. Now `mix test` correctly reports such errors:
```
$ mix test --unknown
** (Mix) Could not invoke task "test": 1 error found!
--unknown : Unknown option
```
Note not all tasks have been updated to use strict option parsing. Some tasks, like `mix compile`, are actually a front-end to many other tasks, and as such, it cannot effectively assert which options are valid.
## ExUnit
ExUnit packs many improvements on the tooling side, better integration with external tools, as well as mechanisms to improve the readability of your tests.
### mix test --stale
ExUnit builds on top of `mix xref` to provide the `mix test --stale` functionality. When the `--stale` flag is given, `mix` will only run the tests that may have changed since the last time you ran `mix test --stale`. For example:
* If you saved a test file on disk, Mix will run that file and ignore the ones that have not changed
* If you changed a library file, for example, `lib/foo.ex` that defines `Foo`, any test that invokes a function in `Foo` directly or indirectly will also run
* If you modify your `mix.exs` or your `test/test_helper.exs`, Mix will run the whole test suite
This feature provides a great workflow for developers, allowing them to effortlessly focus on parts of the codebase when developing new features.
### Diffing
ExUnit will now include diff-ing output every time a developer asserts `assert left == right` in their tests. For example, the assertion:
```elixir
assert "fox jumps over the lazy dog" ==
"brown fox jumps over the dog"
```
will fail with
```elixir
1) test compare (Difference)
lib/ex_unit/examples/difference.exs:10
Assertion with == failed
lhs: "fox jumps over the lazy dog"
rhs: "brown fox jumps over the dog"
stacktrace:
lib/ex_unit/examples/difference.exs:11: (test)
```
in a way that "lazy" in "lhs" will be shown in red to denote it has been removed from "rhs" while "brown" in "rhs" will be shown in green to denote it has been added to the "rhs".
When working with large or nested data structures, the diffing algorithm makes it fast and convenient to spot the actual differences in the asserted values.
### Test types
ExUnit v1.3 includes the ability to register different test types. This means libraries like QuickCheck can now provide functionality such as:
```elixir
defmodule StringTest do
use ExUnit.Case, async: true
use PropertyTestingLibrary
property "starts_with?" do
forall({s1, s2} <- {utf8, utf8}) do
String.starts_with?(s1 <> s2, s1)
end
end
end
```
At the end of the run, ExUnit will also report it as a property, including both the amount of tests and properties:
```
1 property, 10 tests, 0 failures
```
### Named setups and describes
Finally, ExUnit v1.3 includes the ability to organize tests together in describe blocks:
```elixir
defmodule StringTest do
use ExUnit.Case, async: true
describe "String.capitalize/2" do
test "uppercases the first grapheme" do
assert "T" <> _ = String.capitalize("test")
end
The problem with the approach above is that, once more and more functions are added to the `MyApp` module, it becomes increasingly harder to know the purposes of the `init/1` and `call/2` functions. For example, for a developer unfamiliar with Plug, are those functions part of the `MyApp` API or are they implementations of a given callback?
Elixir v1.5 introduces the `@impl` attribute, which allows us to mark that certain functions are implementation of callbacks:
defmodule MyApp do
@behaviour Plug
@impl true
def init(_opts) do
opts
end
@impl true
def call(conn, _opts) do
Plug.Conn.send_resp(conn, 200, "hello world")
end
test "lowercases the remaining graphemes" do
assert "Test" = String.capitalize("TEST")
end
end
end
```
You may even use `@impl Plug` if you want to explicitly document which behaviour defines the callback you are implementing.
Every test inside a describe block will be tagged with the describe block name. This allows developers to run tests that belong to particular blocks, be them in the same file or across many files:
Overall, using `@impl` has the following advantages:
```
$ mix test --only describe:"String.capitalize/2"
```
* Readability of the code is increased, as it is now clear which functions are part of your API and which ones are callback implementations. To reinforce this idea, `@impl true` automatically marks the function as `@doc false`, disabling documentation unless `@doc` is explicitly set
Note describe blocks cannot be nested. Instead of relying on hierarchy for composition, we want developers to build on top of named setups. For example:
* If you define `@impl` before a function that is not a callback, Elixir will error. This is useful in case of typos or in case the behaviour definition changes (such as a new major version of a library you depend on is released)
```elixir
defmodule UserManagementTest do
use ExUnit.Case, async: true
* If you use `@impl` in one implementation, Elixir will force you to declare `@impl` for all other implementations in the same module, keeping your modules consistent
describe "when user is logged in and is an admin" do
setup [:log_user_in, :set_type_to_admin]
## Calendar improvements
test ...
end
This release brings further improvements to Calendar types. It adds arithmetic and others functions to `Time`, `Date`, `NaiveDateTime` and `Datetime` as well as conversion between different calendars.
describe "when user is logged in and is a manager" do
setup [:log_user_in, :set_type_to_manager]
## v1.5.2 (2017-09-29)
test ...
end
### 1. Enhacements
defp log_user_in(context) do
# ...
end
end
```
By restricting hierarchies in favor of named setups, it is straight-forward for the developer to glance at each describe block and know exactly the setup steps involved.
## v1.3.4 (2016-10-09)
### 1. Bug fixes
#### Elixir
* [Kernel] Optimize function definition with multiple clauses by not traversing the internal clauses table
* [Kernel] Warn if unary operators are followed by new lines
* [Registry] Use the name of the Registry as its `:id` in the `child_spec/1` function
### 2. Bug fixes
#### Elixir
* [DateTime] Fix negative microsecond result when passing negative Unix epochs to `from_unix/2`
* [Kernel] Improve error message for oversized atoms
* [Kernel] Ensure `@impl` attribute also propagates to clauses from default arguments
* [Kernel] Emit proper error for unknown vars inside binary pattern in match
#### IEx
* [IEx] Do not crash IEx unexpectedly on `System.stop/0`
* [IEx.Helpers] Ensure exiting a breakpoint set inside a breakpoint does not terminate the shell unexpectedly
#### Mix
* [mix local.hex] Ensure `--if-missing` flag works as advertised
* [mix test] Do not trigger additional error reports when there is a failure when loading test files
* [Mix.SCM.Git] Ensure errors when invoking `git` propagate correctly
## v1.5.1 (2017-08-01)
### 1. Enhancements
#### EEx
* [EEx.Engine] Add `handle_begin` and `handle_end` to EEx
#### Elixir
* [Kernel] Do not use references on function/macro definitions - this provides large improvements in compilation times in some rare corner cases
* [Supervisor] Support mixing old and new typespecs in `Supervisor.init/2` and `Supevisor.start_link/2`
#### Mix
* [mix profile] Allow profile tasks to run without a project
### 2. Bug fixes
#### EEx
* [EEx.Engine] Do not re-use the value of the `init/1` callback throughout the compilation stack
#### Elixir
* [Kernel] Ensure dialyzer does not emit warnings in some uses of `with`
* [Kernel] Fix dialyzer warnings when `defmacrop` is used in modules
* [Kernel] Ensure Elixir modules can be dialyzed without starting the Elixir application
* [Kernel] Do not serialize references in quoted expressions
* [Kernel] Make sure structs expansion use the latest definition available when struct modules are recompiled
* [Task] Support `:infinity` timeout on Task streams
* [Typespec] Ensure typespecs allow `tuple` to be used as variable names
## v1.5.0 (2017-07-25)
### 1. Enhancements
#### Elixir
* [Access] Optimize `Access.get/2`
* [Base] Optimise Base encode/decode
* [Calendar] Implement Inspect for DateTime with Calendar.ISO
* [Calendar] Add "ISO days" format for conversions between Calendars and `Date.convert/2`, `Time.convert/2`, `NaiveDateTime.convert/2` and `DateTime.convert/2` (as well as bang variants)
* [Calendar] Add `:calendar` field to `Time` struct
* [Calendar] Add `Time.diff/3`, `Date.add/2`, `Date.diff/2`, `DateTime.diff/3`
* [Calendar] Add `Date.range/2`
* [Calendar] Add `Date.new/4`, `DateTime.utc_now/1`, `NaiveDateTime.new/8` and `Time.new/5` that allow specifing calendar
* [Enum] Add `Enum.chunk_by/4` and `Stream.chunk_by/4`
* [Enum] Add `Enum.chunk_every/2` and `Enum.chunk_every/4` with a more explicit API than `Enum.chunk/2` and `Enum.chunk/4`
* [Exception] Add `Exception.blame/3` that adds metadata to exceptions
* [File] Add `File.read_link/1` and `File.read_link!/1`
* [File] Introduce `:trim_bom` option for `File.stream!/2`
* [Inspect] Add `:printable_limit` to control the limit of printable structures
* [Integer] Add `Integer.gcd/2`
* [Kernel] Add `left not in right` to check that the left side is not in the enumerable on the right
* [Kernel] Use the new `debug_info` chunk in OTP 20. This provides a mechanism for tools to retrieve the Elixir AST from beam files
* [Kernel] `defoverridable/1` accepts a module name as argument and marks all callbacks as overridable
* [Kernel] Allow non-quoted Unicode atoms and variables according to Unicode Annex #31 (see Unicode Syntax document)
* [Kernel] Warn when a `:__struct__` key is used when building/updating structs
* [Kernel] Cache the AST on definitions. This speeds up the compilation time from 10% to 15% measured across different projects
* [Kernel] Improve compiler error message on invalid patterns and guards
* [Keyword] Add `replace/3` and `replace!/3` for replacing an existing key
* [List] `List.starts_with?/2`
* [Macro] Introduce `Macro.generate_arguments/2`
* [Map] Optimize `Map.merge/3` by choosing merging direction
* [Map] Add `replace/3` and `replace!/3` for replacing an existing key
* [Map] Raise `BadMapError` in `Map.equal?/2` when either of the two arguments is not a map
* [MapSet] Reduce `MapSet` size when serialized to approximately half
* [Process] Add `Process.cancel_timer/2`
* [Protocol] Show available implementations on `Protocol.UndefinedError` if the protocol has been consolidated
* [Registry] Support ETS guard conditions in `Registry.match/3`
* [Registry] Support `parallel: true` in `Registry.dispatch/3`
* [Registry] Introduce `Registry.unregister_match/4`
* [Stream] Add `Stream.chunk_every/2` and `Stream.chunk_every/4` with a more explicit API than `Stream.chunk/2` and `Stream.chunk/4`
* [String] Optimise binary pattern matching in `String.split/1` and `String.trim_*/1`
* [Supervisor] Add `Supervisor.init/2` and `Supervisor.child_spec/2`
* [Supervisor] Allow `module` and `{module, arg}` to be given to `Supervisor.start_link/2` and invoke `module.child_spec(arg)` on each argument
* [Task] Support `:on_timeout` in `Task.async_stream` to control how tasks are terminated
* [Task] Add `ordered: false` support to `Task.async_stream`
* [Kernel] Ensure the compiler does not generate unecessary variable bindings inside case statements. This improves the code emitted and make sure "unused variable warnings" are not mistakenly silenced
* [Kernel] Move `raise` checks to runtime to avoid crashing cover on Erlang 19.1
* [Protocol] Do not emit warnings when using protocols on opaque types
#### ExUnit
* [ExUnit] Show code snippet from test source file in case of test errors
* [ExUnit] Use `Exception.blame/3` when formatting test errors
* [ExUnit] Make `assert_raise/2` fail if the underlying exception has a broken `message/1` implementation
* [ExUnit] Add `start_supervised/2` and `stop_supervised/1` to ExUnit. Processes started by this function are automatically shut down when the test exits
* [ExUnit.CaptureLog] Flush Erlang's `:error_logger` before capturing to avoid mixed messages
#### IEx
## v1.3.3 (2016-09-17)
* [IEx.Autocomplete] Support autocompletion of variable names
* [IEx.Autocomplete] Support autocompletion of functions imported using `import Mod, only: [...]`
* [IEx.Evaluator] Use `Exception.blame/3` when showing errors in the terminal
* [IEx.Helpers] Add `exports/1` IEx helper to list all exports in a module
* [IEx.Helpers] Add `break!/2`, `break!/4`, `breaks/0`, `continue/0`, `open/0`, `remove_breaks/0`, `remove_breaks/1`, `reset_break/1`, `reset_break/3` and `whereami/1` for code debugging
* [IEx.Helpers] No longer emit warnings for IEx commands without parentheses
* [IEx.Helpers] Add `runtime_info/0` for printing runtime system information
* [IEx.Helpers] Add `open/1` to open the source of a given module/function in your editor
* [IEx.Info] Implement `IEx.Info` protocol for calendar types
### 1. Enhancements
#### Elixir
* [DateTime] Support negative integer in `DateTime.from_unix/2`
* [Kernel.LexicalTracker] Do not consider remote typespecs as a compile-time dependency
* [Kernel.ParallelCompiler] Do not emit deadlock messages when the process is waiting on itself
* [Kernel.Typespec] Mark struct update syntax as generated to avoid false positives from dialyzer
#### ExUnit
* [ExUnit] Make ExUnit server timeout configurable
#### Logger
* [Logger] Add `metadata: :all` configuration to log all metadata
* [Logger] Use `:ansi_color` if one is available in metadata
#### Mix
* [mix compile.elixir] Add `--all-warnings` option to Elixir compiler that shows all warnings from the previous compilation (instead of just of the files being compiled)
* [mix escript.build] Strip debug information from escripts by default and add option `:strip_beam` which defaults to true
* [mix loadpaths] Ensure `--no-deps-check` do not trigger SCM callbacks (such as `git`)
* [mix local.hex] Add `--if-missing` flag to `local.hex` mix task
* [mix profile.cprof] Add `Mix.Tasks.Profile.Cprof` for count-based profiling
* [mix new] New styling for generated applications
* [Mix] Add support for the `:sparse` option in `Mix.SCM.Git`
* [Mix] Skip dependendency loading if `MIX_NO_DEPS` is set to `1`
### 2. Bug fixes
#### Elixir
* [Calendar] Ensure `Calendar.ISO` raises a readable error when reaching up the year 10000 restriction
* [Calendar] Return `{:error, :invalid_time}` for wrong precision instead of crashing when parsing ISO dates
* [Enumerable] Raise `Protocol.UndefinedError` on bad functions in Enumerable implementation
* [File] Ensure recursive file operations raise on paths with null bytes (*security issue reported by Griffin Byatt*)
* [File] Support `:ram`/`:raw` files in `File.copy/2`
* [Inspect] Do not use colors when inspecting error messages
* [Kernel] Support guards on anonymous functions of zero arity
* [Kernel] Fix compilation of maps used as maps keys inside matches
* [Kernel] Ensure `do` clause in `with` is tail call optimizable
* [Module] `on_definition/6` callback receives body wrapped in a keyword list, such as `[do: body]`. This solves a bug where it was impossible to distinguish between a bodyless clause and a function that returns `nil`.
* [Path] Ensure recursive path operations raise on paths with null bytes (*security issue reported by Griffin Byatt*)
* [Protocol] Do not lose source compile info on protocol consolidation
* [Record] Properly escape quoted expressions passed to `defrecord`
* [Regex] Fix `inspect/2` for regexes with `/` terminator in them
* [Registry] Ensure `Registry.match/4` works with `:_` as key
* [Stream] Fix stream cycle over empty enumerable
* [String] Consider Unicode non-characters valid according to the specification in `String.valid?/1`
* [StringIO] Fix encoding and performance issues in `StringIO.get_until`
* [System] Raise on paths with null bytes in `System.cmd/2` and in `System.find_executable/1` (*security issue reported by Griffin Byatt*)
* [System] Raise on ill-formed environment variables (*security issue reported by Griffin Byatt*)
#### ExUnit
* [ExUnit] Properly account failed tests when `setup_all` fails
* [ExUnit] Having two or more `describe` blocks with the same name will now raise an error.
* [System] Use `NUL` instead of `/dev/null` on Windows when building `System.build_info`
#### IEx
* [IEx] Skip autocompletion of module names that are invalid without being quoted
* [IEx] Skip autocompletion of functions with default arguments with `@doc false`
* [IEx] Do not start oldshell alongside IEx
* [IEx.Autocomplete] Resolves issue with autocompletion on structs not working
#### Mix
* [mix compile.elixir] Store multiple sources in case of module conflicts. This solves an issue where `_build` would get corrupted when compiling Elixir projects with module conflicts
* [mix compile.erlang] Do not silently discard Erlang compile errors
* [mix compile.erlang] Properly track `-compile` module attribute when specified as a list
* [mix compile.protocols] Ensure protocol implementations do not "disappear" when switching between applications in umbrella projects by having separate consolidation paths per project
* [mix compile.protocols] Do not raise when consolidating a protocol that was converted into a module
* [Mix] Also store external resources that are not part of the current working directory in compilation manifest
* [Mix] Always include the compiled file source in manifests
### 3. Soft deprecations (no warnings emitted)
## v1.3.2 (2016-07-15)
### 1. Enhancements
#### Elixir
* [Kernel] `not left in right` is soft-deprecated in favor of `left not in right`
* [Kernel] Support guards in `else` clauses in `with`
### 4. Deprecations
#### Mix
* [Mix] Add `MIX_NO_DEPS` env var for disabling dep loading. Used for third-party scripts and tools like Nix package manager
* [Mix] Add `mix test --listen-on-stdin` that automatically reruns tests on stdin
* [Mix] Disable `--warnings-as-errors` when compiling dependencies
* [Mix] Add `--filter` option to `mix deps.unlock` to unlock only matching dependencies
### 2. Bug fixes
#### Elixir
* `Atom.to_char_list/1`, `Float.to_char_list/1`, `Integer.to_char_list/1`, `Integer.to_char_list/2`, `Kernel.to_char_list/1`, `List.Chars.to_char_list/1`, `String.to_char_list/1` have been deprecated in favor of their `to_charlist` version. This aligns with the naming conventions in both Erlang and Elixir
* [Enum] Deprecate `Enum.filter_map/3` in favor of `Enum.filter/2` + `Enum.map/2` or for-comprehensions
* [GenEvent] Deprecate `GenEvent` and provide alternatives in its docs
* [Kernel] Using `()` to mean `nil` is deprecated
* [Kernel] `:as_char_lists value` in `Inspect.Opts.t/0` type, in favor of `:as_charlists`
* [Kernel] `:char_lists` key in `Inspect.Opts.t/0` type, in favor of `:charlists`
* [Module] Using Erlang parse transforms via `@compile {:parse_transform, _}` is deprecated
* [Stream] Deprecate `Stream.filter_map/3` in favor of `Stream.filter/2` + `Stream.map/2`
* [String] `String.ljust/3` and `String.rjust/3` are deprecated in favor of `String.pad_leading/3` and `String.pad_trailing/3` with a binary padding
* [String] `String.strip/1` and `String.strip/2` are deprecated in favor of `String.trim/1` and `String.trim/2`
* [String] `String.lstrip/1` and `String.rstrip/1` are deprecated in favor of `String.trim_leading/1` and `String.trim_trailing/1`
* [String] `String.lstrip/2` and `String.rstrip/2` are deprecated in favor of `String.trim_leading/2` and `String.trim_trailing/2` with a binary as second argument
* [Typespec] `char_list/0` type is deprecated in favor of `charlist/0`
* [Enum] Return `nil` if enumerable halts in `Enum.find_index/3`
* [Kernel] Do not attempt to load modules that have not been required when invoking regular functions, otherwise this invalidates the `@compile {:autoload, false}` directive.
#### Mix
* [Mix] Ensure missing protocol dependencies are discarded in umbrella projects with shared build
#### ExUnit
* [ExUnit.Diff] Ensure no leading or trailing when diffing some maps
## v1.3.1 (2016-06-28)
### 1. Enhancements
#### IEx
* [IEx.Helpers] `Add import_file_if_available` for importing files only if they are available
* [IEx.Helpers] `Add import_if_available` for importing modules only if they are available
### 2. Bug fixes
#### Elixir
* [Kernel] Ensure structs can be expanded in dynamic module names
* [Kernel] Ensure aliases warnings are not accidentally discarded when the same module is imported
* [Kernel.ParallelCompiler] Ensure two modules with cyclic struct dependencies cannot run into a deadlock when compiling
* [Kernel.Typespec] Support module attributes in remote types
* [Module] Do not expect stacktraces to be always present when dispatching to locals during the module compilation
#### IEx
* [IEx.Helpers] Fix `h` helper for operators
#### Mix
* [Mix] Do not load modules for xref purposes, instead use BEAM info
* [Mix] Ensure `deps.check` does not check archives (that's done in loadpaths)
* [Mix] Validate application properties before traversing them
* [Mix] Check for proper Makefile when compiling on Windows
* [Mix] Enforce space after comma in `mix do`
## v1.3.0 (2016-06-21)
### 1. Enhancements
#### EEx
* [EEx] Deprecate `<%= ` in "middle" and "end" expressions, e.g.: `<%= else %>` and `<%= end %>`
* [EEx.Engine] Support an `init/1` function in engines that will return the initial buffer (defaults to an empty string)
## v1.4
#### Elixir
The CHANGELOG for v1.4 releases can be found [in the v1.4 branch](https://github.com/elixir-lang/elixir/blob/v1.4/CHANGELOG.md).
* [Access] Add support for `Access.all/0`, `Access.elem/1`, `Access.key/2` and `Access.key!/1` for traversing nested data structures
* [Calendar] Add `Calendar` and `Date`, `Time`, `NaiveDateTime` and `DateTime` types
* [CLI] Add `--logger-otp-reports BOOL` and `--logger-sasl-reports BOOL` switches
* [Compiler] Emit a summary of compilation errors when modules are missing
* [Enum] Add `Enum.group_by/3` that allows developers to map on the value being grouped
* [Enum] Make list values in maps returned by `Enum.group_by/2` and `Enum.group_by/3` preserve the order of the input enumerable instead of reversing it.
* [Enum] Add `Enum.drop_every/2` that drops every `nth`, including the first one
* [Exception] Suggest possible functions on `UndefinedFunctionError` for existing modules
* [Exception] Warn if unknown fields are given to `raise/2`
* [File] Support IO devices in `File.copy/3`
* [GenServer] Raise a more meaningful exit if you try to `GenServer.call/3` yourself
* [Inspect] Support `:base` option when inspecting binaries
* [IO] Add `IO.warn/2` that will print a warning message with stacktrace and notify the compiler a warning was printed (in case --warnings-as-errors was enabled)
* [Kernel] Support `generated: true` in quote
* [Kernel] Support `Kernel.pop_in/1` and `Kernel.pop_in/2` for yanking a value from a nested data structure
* [Kernel] Allow variable struct names when matching, for example, `%module{key: "value"} = struct`
* [Kernel] Allow guards on the left side of `<-` in `for` and `with` special forms
* [Kernel] Support `else` chunks in `with`
* [Kernel] Track `{module, function, arity}` imports and warn on unused ones when such are specified in `:only`
* [Kernel] Add `keyword/0` and `keyword/1` built-in types to typespecs
* [Kernel] Add sigils for date (`~D[2015-04-17]`), time (`~T[08:00:00]`) and naive date times `~N[2015-04-17 08:00:00]`
* [Kernel] Support `@enforce_keys` on `defstruct/1` to guarantee some keys are explicitly given when building structs
* [OptionParser] Add support for `:count` switch type
* [OptionParser] Add `parse!/2` and `parse_head!/2` that raise `OptionParser.ParseError` in case of errors
* [Process] Add `Process.sleep/1`
* [Range] `Range.range?/1` now checks the validity of a range.
* [Regex] Support `:include_captures` in `Regex.split/3`
* [String] Add `String.myers_difference/2` for calculating the difference between two strings
* [System] Add `System.os_time/0` and `System.os_time/1`
* [Typespec] Add support for `%{required(foo) => bar}` and `%{optional(foo) => bar}` forms (Erlang 19 only)
* [Typespec] Add support for `@optional_callbacks` to mark certain that certain callbacks may be optionally implemented
* [Typespec] Introduce `%{...}` to mean any map (Erlang 19 only)
* [URI] Add `URI.merge/2`
* [Version] Add `Version.parse!/1`
#### ExUnit
* [ExUnit] Show pinned variables on failed `assert ^left = right` and `assert match?(^left, right)` assertions
* [ExUnit] Add `ExUnit.Case.register_attribute` which allow attributes to be cleaned up whenever a test is defined
* [ExUnit] Add `ExUnit.Case.register_test` and support the ability to tag "tests" by type. This will allow projects like QuickCheck to change the wording in formatters to say "10 properties" instead of "10 tests"
* [ExUnit] Support diffing of values when using `==` in `assert`
* [ExUnit] Start running tests as soon as cases are loaded. This feature is enabled by default when running tests through Mix
* [ExUnit] Raise a straight-forward error message in case a duplicate test name is defined
* [ExUnit] Bump the default number of max cases to double of schedulers to support both IO and CPU bound tests
* [ExUnit] Support for named setups in `setup` and `setup_all`
* [ExUnit] Support for bundling tests together with `describe/2`
#### IEx
* [IEx] Add `nl/2` that loads a given module on a list of nodes
* [IEx.Helpers] No longer restart applications on `recompile/1`
* [IEx.Autocomplete] Improve IEx expand to handle functions after `&`
#### Logger
* [Logger] Introduce `Logger.reset_metadata/0,1`
#### Mix
* [Mix] Add `mix xref` and `mix compile.xref` that runs cross-reference checks, with the latter running after compilation by default
* [Mix] Add `mix app.tree` and `mix deps.tree`
* [Mix] Add `Mix.Task.rerun/2` that reenables and re-runs a task
* [Mix] Integrate `OptionParser.ParseError` into Mix, automatically converting such exceptions into `Mix.Error` and embedding the task information
* [Mix] Support `@preferred_cli_env` attribute when defining tasks
* [Mix] Support `mix test --raise` that will raise when a test suite fails (instead of setting the exit code to 1)
* [Mix] Enable rebar3 manager by default for Hex dependencies
* [Mix] Add `mix escript.install` to install escripts
* [Mix] Print stacktraces for `Mix.Error` when `MIX_DEBUG=1` is set
* [Mix] Add a user friendly error for merge conflicts on `mix.lock`
* [Mix] Track files between path dependencies. This means umbrella applications will no longer trigger full recompilation when a sibling changes. Instead it will only recompile the files affected by the sibling changes
* [Mix] No longer print every file being compiled. Instead a generic "Compiling N files (.ext)" will be printed and files will only be logged in case they take more than 5 seconds to compile. This threshold can be customized by passing the `--long-compilation-threshold` flag and the previous behaviour can be reenabled by giving `--verbose` to `mix compile`
* [Mix] Add `mix test --stale` that uses static analysis on source files to know which tests should run when source files changes. If any test file changes, it will also re-run. Changing a configuration file or the test helper will trigger a full recompilation
### 2. Bug fixes
#### Elixir
* [Application] Ensure `Application.spec/2` returns nil for unknown applications
* [GenServer] Ensures `cast/2` returns `:ok` if locally registered process is not found
* [Inspect] Ensure binaries break into new lines when inspected
* [Kernel] Do not choke on capture operator with argument above `&191`
* [Kernel] Raise if `defstruct` is called multiple times
* [Kernel] Ensure `Module.create/3` respects var/alias hygiene
* [Kernel] Support non-literal ranges on the right side of `in/2`
* [Macro] Fix `Macro.to_string/1` on a call of a capture argument, for example `&(&1).(:x)`
* [OptionParser] Allow `OptionParser` to parse negative numbers
* [Record] Fix `Record.is_record/2` when dealing with non-record tuples
* [String] Ensure `strip` also removes non-breaking whitespaces (and ensure `split` still does not split on them)
* [URI] Use square brackets for IPv6 in `URI.to_string/1`
#### Mix
* [Mix] Improve task not found message when Mix would include the not found task as a suggestion due to different casing
* [Mix] Ignore lock revision when the lock is out of date when updating Mix dependencies. Before this fix, Git tags and branches in the lock file would erroneously take higher precedence than the one in `mix.exs`
* [Mix] Only recompile empty Elixir files if they change instead of recompiling them on every run
* [Mix] Ensure .app file is written in UTF-8 (this allows app descriptions to contain UTF-8 characters)
* [Mix.Dep] Always specify the `:env` option internally for dependencies to avoid false positives in the dependency resolution
* [Mix.Dep] Correctly detect conflict from cousin optional dependencies in the dependency resolution algorithm
### 3. Soft deprecations (no warnings emitted)
* [Float] `Float.to_string/2` and `Float.to_char_list/2` has been soft-deprecated as Elixir will now attempt to print the shortest and most accurate representation by default. Developers can always fallback to `:erlang.float_to_binary/2` and `:erlang.float_to_list/2` if they need the previous functionality
* [Kernel] `to_char_list` functions have been soft-deprecated in favor of `to_charlist`. This aligns with the naming conventions in both Erlang and Elixir
* [String] The confusing `String.strip/2`, `String.lstrip/2` and `String.rstrip/2` API has been soft deprecated in favor of `String.trim/2`, `String.trim_leading/2` and `String.trim_trailing/2`
* [String] The confusing `String.ljust/3` and `String.rjust/3` API has been soft deprecated in favor of `String.pad_leading/3` and `String.pad_trailing/3`
* [Typespec] `char_list` is soft-deprecated in favor of `charlist`
### 4. Deprecations
This release deprecates many APIs that have been soft-deprecated in previous Elixir versions.
#### Elixir
* [Dict] `Dict` is no longer a behaviour and its functions will be deprecated in upcoming releases
* [Enum] Passing a dictionary to `Enum.group_by/3` is deprecated
* [Kernel] `\x{H*}` in strings/sigils/charlists is deprecated
* [Kernel] Add deprecation for `defdelegate` list arguments and `:append_first` option. The previously undocumented and deprecated support for matching has been removed
* [Kernel] Warn if a variable is assigned inside `case`/`if`/etc and used outside the block
* [Keyword] `Keyword.size/1` is deprecated in favor of `Kernel.length/1`
* [Map] `Map.size/1` is deprecated in favor of `Kernel.map_size/1`
* [Regex] The option `/r` (for ungreedy) has been deprecated in favor of `/U`
* [Set] `Set` is no longer a behaviour and its functions will be deprecated in upcoming releases
* [String] `String.valid_character?/1` is deprecated in favor of `String.valid?/1` with pattern matching
* [Task] `Task.find/2` is deprecated in favor of explicit message matching
* [URI] Passing a non-map to `URI.decode_query/2` is deprecated
+3 -3
View File
@@ -1,14 +1,14 @@
### Precheck
* Do not use the issues tracker for help or support (try Elixir Forum, Stack Overflow, IRC, etc.)
* Do not use the issues tracker for help or support (try Stack Overflow, IRC, mailing list, etc)
* For proposing a new feature, please start a discussion on the Elixir Core mailing list
* For bugs, do a quick search and make sure the bug has not yet been reported
* Finally, be nice and have fun!
### Environment
* Elixir & Erlang versions (elixir --version):
* Operating system:
* Elixir version (elixir -v):
* Operating system:
### Current behavior
+49 -61
View File
@@ -1,14 +1,13 @@
REBAR ?= "$(CURDIR)/rebar"
PREFIX ?= /usr/local
SHARE_PREFIX ?= $(PREFIX)/share
CANONICAL := v1.5/
DOCS := v1.3
CANONICAL := stable
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict
ERLC := erlc -I lib/elixir/include
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
VERSION := $(strip $(shell cat VERSION))
Q := @
LIBDIR := lib
BINDIR := bin
INSTALL = install
INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644
@@ -16,16 +15,16 @@ INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
.PHONY: install compile erlang elixir build_plt clean_plt dialyze test clean clean_residual_files install_man clean_man docs Docs.zip Precompiled.zip zips
.PHONY: install compile erlang elixir build_plt clean_plt dialyze test clean install_man clean_man docs Docs.zip Precompiled.zip publish_zips publish_docs publish_mix
.NOTPARALLEL: compile
#==> Functions
define CHECK_ERLANG_RELEASE
$(Q) erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 18)])' -s erlang halt | grep -q '^true'; \
$(Q) erl -noshell -eval 'io:fwrite("~s", [erlang:system_info(otp_release) >= "18"])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \
echo "At least Erlang 18.0 is required to build Elixir"; \
exit 1; \
echo "At least Erlang 18.0 is required to build Elixir"; \
exit 1; \
fi;
endef
@@ -60,38 +59,36 @@ compile: lib/elixir/src/elixir.app.src erlang elixir
lib/elixir/src/elixir.app.src: src/elixir.app.src
$(Q) $(call CHECK_ERLANG_RELEASE)
$(Q) rm -f lib/elixir/src/elixir.app.src
$(Q) rm -rf lib/elixir/src/elixir.app.src
$(Q) echo "%% This file is automatically generated from <project_root>/src/elixir.app.src" \
>lib/elixir/src/elixir.app.src
>lib/elixir/src/elixir.app.src
$(Q) cat src/elixir.app.src >>lib/elixir/src/elixir.app.src
erlang:
$(Q) cd lib/elixir && $(REBAR) compile
# Since Mix depends on EEx and EEx depends on Mix,
# we first compile EEx without the .app file,
# then mix and then compile EEx fully
# Since Mix depends on EEx and EEx depends on
# Mix, we first compile EEx without the .app
# file, then mix and then compile EEx fully
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
$(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
$(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler core -s erlang halt; \
fi
@ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/kernel.ex" -o ebin;
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
$(Q) $(MAKE) unicode
$(Q) rm -f lib/elixir/ebin/elixir.app
$(Q) rm -rf lib/elixir/ebin/elixir.app
$(Q) cd lib/elixir && $(REBAR) compile
unicode: $(UNICODE)
$(UNICODE): lib/elixir/unicode/*
@ echo "==> unicode (compile)";
$(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/properties.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(Q) cd lib/elixir && ../../$(ELIXIRC) unicode/unicode.ex -o ebin;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger))
@@ -102,15 +99,15 @@ $(eval $(call APP_TEMPLATE,iex,IEx))
install: compile
@ echo "==> elixir (install)"
$(Q) for dir in lib/*; do \
rm -rf $(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin; \
rm -Rf $(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin; \
$(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin"; \
$(INSTALL_DATA) $$dir/ebin/* "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/$$dir/ebin"; \
done
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/bin"
$(Q) $(INSTALL_PROGRAM) $(filter-out %.ps1, $(filter-out %.bat, $(wildcard bin/*))) "$(DESTDIR)$(PREFIX)/$(LIBDIR)/elixir/bin"
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/$(BINDIR)"
$(Q) for file in "$(DESTDIR)$(PREFIX)"/$(LIBDIR)/elixir/bin/*; do \
ln -sf "../$(LIBDIR)/elixir/bin/$${file##*/}" "$(DESTDIR)$(PREFIX)/$(BINDIR)/"; \
$(Q) $(INSTALL_DIR) "$(DESTDIR)$(PREFIX)/bin"
$(Q) for file in "$(DESTDIR)$(PREFIX)"/$(LIBDIR)/elixir/bin/* ; do \
ln -sf "../$(LIBDIR)/elixir/bin/$${file##*/}" "$(DESTDIR)$(PREFIX)/bin/" ; \
done
$(MAKE) install_man
@@ -118,36 +115,29 @@ clean:
cd lib/elixir && $(REBAR) clean
rm -rf ebin
rm -rf lib/*/ebin
rm -f lib/elixir/src/elixir.app.src
$(Q) $(MAKE) clean_residual_files
rm -rf lib/elixir/test/ebin
rm -rf lib/*/tmp
rm -rf lib/mix/test/fixtures/git_repo
rm -rf lib/mix/test/fixtures/deps_on_git_repo
rm -rf lib/mix/test/fixtures/git_rebar
rm -rf lib/elixir/src/elixir.app.src
$(MAKE) clean_man
clean_elixir:
clean_exbeam:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
clean_residual_files:
rm -rf lib/*/_build/
rm -rf lib/*/tmp/
rm -rf lib/elixir/test/ebin/
rm -rf lib/mix/test/fixtures/deps_on_git_repo/
rm -rf lib/mix/test/fixtures/git_rebar/
rm -rf lib/mix/test/fixtures/git_repo/
rm -rf lib/mix/test/fixtures/git_sparse_repo/
rm -f erl_crash.dump
$(Q) $(MAKE) clean_man
#==> Documentation tasks
#==> Create Documentation
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}\c")
DOCS_FORMAT = html
COMPILE_DOCS = bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" -m "$(3)" -u "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) -o doc/$(2) -n https://hexdocs.pm/$(2)/$(CANONICAL) -p http://elixir-lang.org/docs.html -f "$(DOCS_FORMAT)" $(4)
COMPILE_DOCS = bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" -m "$(3)" -u "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) -o doc/$(2) -a http://elixir-lang.org/docs/$(CANONICAL)/$(2)/ -p http://elixir-lang.org/docs.html $(4)
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
docs_elixir: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (elixir)"
$(Q) rm -rf doc/elixir
$(call COMPILE_DOCS,Elixir,elixir,Kernel,-e "lib/elixir/pages/Behaviours.md" -e "lib/elixir/pages/Deprecations.md" -e "lib/elixir/pages/Guards.md" -e "lib/elixir/pages/Naming Conventions.md" -e "lib/elixir/pages/Operators.md" -e "lib/elixir/pages/Syntax Reference.md" -e "lib/elixir/pages/Typespecs.md" -e "lib/elixir/pages/Unicode Syntax.md" -e "lib/elixir/pages/Writing Documentation.md")
$(call COMPILE_DOCS,Elixir,elixir,Kernel,-e "lib/elixir/pages/Behaviours.md" -e "lib/elixir/pages/Naming Conventions.md" -e "lib/elixir/pages/Typespecs.md" -e "lib/elixir/pages/Writing Documentation.md")
docs_eex: compile ../ex_doc/bin/ex_doc
@ echo "==> ex_doc (eex)"
@@ -178,30 +168,30 @@ docs_logger: compile ../ex_doc/bin/ex_doc
@ echo "ex_doc is not found in ../ex_doc as expected. See README for more information."
@ false
#==> Zip tasks
#==> Zips
Docs.zip: docs
rm -f Docs-v$(VERSION).zip
rm -rf Docs-v$(VERSION).zip
zip -9 -r Docs-v$(VERSION).zip CHANGELOG.md doc NOTICE LICENSE README.md
@ echo "Docs file created $(CURDIR)/Docs-v$(VERSION).zip"
Precompiled.zip: build_man compile
rm -f Precompiled-v$(VERSION).zip
rm -rf Precompiled-v$(VERSION).zip
zip -9 -r Precompiled-v$(VERSION).zip bin CHANGELOG.md lib/*/ebin LICENSE man NOTICE README.md VERSION
@ echo "Precompiled file created $(CURDIR)/Precompiled-v$(VERSION).zip"
zips: Precompiled.zip Docs.zip
#==> Publish
#==> Test tasks
publish_zips: Precompiled.zip Docs.zip
publish_docs: docs
rm -rf ../docs/$(DOCS)/*/
cp -R doc/* ../docs/$(DOCS)
#==> Tests tasks
test: test_erlang test_elixir
test_windows: test test_taskkill
test_taskkill:
taskkill //IM erl.exe //F //T //FI "MEMUSAGE gt 0"
taskkill //IM epmd.exe //F //T //FI "MEMUSAGE gt 0"
TEST_ERL = lib/elixir/test/erlang
TEST_EBIN = lib/elixir/test/ebin
TEST_ERLS = $(addprefix $(TEST_EBIN)/, $(addsuffix .beam, $(basename $(notdir $(wildcard $(TEST_ERL)/*.erl)))))
@@ -252,24 +242,22 @@ man/iex.1:
$(Q) cp man/iex.1.in man/iex.1
$(Q) sed -i.bak "/{COMMON}/r common" man/iex.1
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
$(Q) rm -f man/iex.1.bak
$(Q) rm man/iex.1.bak
man/elixir.1:
$(Q) cp man/elixir.1.in man/elixir.1
$(Q) sed -i.bak "/{COMMON}/r common" man/elixir.1
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
$(Q) rm -f man/elixir.1.bak
$(Q) rm man/elixir.1.bak
clean_man:
rm -f man/elixir.1
rm -f man/elixir.1.bak
rm -f man/iex.1
rm -f man/iex.1.bak
install_man: build_man
$(Q) mkdir -p $(DESTDIR)$(SHARE_PREFIX)/man/man1
$(Q) $(INSTALL_DATA) man/elixir.1 $(DESTDIR)$(SHARE_PREFIX)/man/man1
$(Q) $(INSTALL_DATA) man/elixirc.1 $(DESTDIR)$(SHARE_PREFIX)/man/man1
$(Q) $(INSTALL_DATA) man/iex.1 $(DESTDIR)$(SHARE_PREFIX)/man/man1
$(Q) $(INSTALL_DATA) man/mix.1 $(DESTDIR)$(SHARE_PREFIX)/man/man1
$(Q) mkdir -p $(DESTDIR)$(PREFIX)/share/man/man1
$(Q) $(INSTALL_DATA) man/elixir.1 $(DESTDIR)$(PREFIX)/share/man/man1
$(Q) $(INSTALL_DATA) man/elixirc.1 $(DESTDIR)$(PREFIX)/share/man/man1
$(Q) $(INSTALL_DATA) man/iex.1 $(DESTDIR)$(PREFIX)/share/man/man1
$(Q) $(INSTALL_DATA) man/mix.1 $(DESTDIR)$(PREFIX)/share/man/man1
$(MAKE) clean_man
+14 -62
View File
@@ -1,16 +1,12 @@
![Elixir](https://github.com/elixir-lang/elixir-lang.github.com/raw/master/images/logo/logo.png)
=========
[![Travis build](https://secure.travis-ci.org/elixir-lang/elixir.svg?branch=master
[![Build Status](https://secure.travis-ci.org/elixir-lang/elixir.svg?branch=master
"Build Status")](https://travis-ci.org/elixir-lang/elixir)
[![Windows build](https://ci.appveyor.com/api/projects/status/macwuxq7aiiv61g1?svg=true)](https://ci.appveyor.com/project/josevalim/elixir)
Elixir is a dynamic, functional language designed for building scalable and maintainable applications.
For more about Elixir, installation and documentation,
[check Elixir's website](http://elixir-lang.org/).
## Compiling from source
## Usage
To run Elixir from source, clone this repository to your machine, compile and test it:
@@ -28,8 +24,9 @@ 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`.
If tests pass, you are ready to move on to the [Getting Started guide][1]
or to try Interactive Elixir by running `bin/iex` in your terminal.
If tests pass, you are ready to move on to the
[Getting Started guide][1] or to try Interactive Elixir by running:
`bin/iex` in your terminal.
However, if tests fail, it is likely you have an outdated Erlang version
(Elixir requires Erlang 18.0 or later). You can check your Erlang version
@@ -37,20 +34,15 @@ by calling `erl` in the command line. You will see some information as follows:
Erlang/OTP 18 [erts-7.0] [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]
If you have properly set up your dependencies and tests still fail,
you may want to open up a bug report, as explained next.
## Bug reports
For reporting bugs, [visit our issues tracker][2] and follow the steps
for reporting a new issue. Please disclose security vulnerabilities
privately at elixir-security@googlegroups.com.
If you have the correct version and tests still fail, please
[open an issue][2].
## Contributing
We welcome everyone to contribute to Elixir and help us tackle existing issues!
To do so, there are a few things you need to know about the code. First, Elixir
code is divided in applications inside the `lib` folder:
We welcome everyone to contribute to Elixir and help us tackle
existing issues! To do so, there are a few things you need to know
about the code. First, Elixir code is divided in applications inside
the `lib` folder:
* `elixir` - Contains Elixir's kernel and stdlib
@@ -66,9 +58,7 @@ code is divided in applications inside the `lib` folder:
You can run all tests in the root directory with `make test` and you can
also run tests for a specific framework `make test_#{NAME}`, for example,
`make test_ex_unit`. If you just changed something in the Elixir's standard
library, you can run only that portion through `make test_stdlib`, as
`test_elixir` also runs tests for the other projects (EEx, ExUnit, etc.).
`make test_ex_unit`.
In case you are changing a single file, you can compile and run tests only
for that particular file for fast development cycles. For example, if you
@@ -79,28 +69,6 @@ bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_test.exs
```
To recompile (including Erlang modules):
```sh
make compile
```
If your contribution fails the build during the bootstrapping of the language,
you can reproduce it locally by deleting all of Elixir beam files and compiling
again:
```sh
make clean_elixir compile
```
Or to rebuild everything from scratch without running tests:
```sh
make clean compile
```
More tasks can be found by reading the [Makefile](./Makefile).
After your changes are done, please remember to run the full suite with
`make test`.
@@ -118,28 +86,12 @@ case you are looking for some examples:
* [Implement capture_io for ExUnit – Pull Request](https://github.com/elixir-lang/elixir/pull/1059)
We usually keep a list of enhancements and bugs [in the issue tracker][2].
For proposing new features, please start a discussion in the
For proposing a new feature, please start a discussion in the
[Elixir Core mailing list][3]. Keep in mind that it is your responsibility
to argue and explain why a feature is useful and how it will impact the
codebase and the community. Finally, remember all interactions in our official
spaces follow our [Code of Conduct][7].
### Reviewing changes
Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. In case any changes are necessary, the team will leave
appropriate comments requesting changes to the code.
The Elixir team may optionally assign someone to review a pull request.
In case someone is assigned, they must explicitly approve the code before
another team member can merge it.
When review is completed, your pull request will be squashed and merged
into the repository.
## Building documentation
Building the documentation requires [ExDoc](https://github.com/elixir-lang/ex_doc)
@@ -154,7 +106,7 @@ cd ../elixir && make docs
This will produce documentation sets for `elixir`, `mix`, etc., under
the `doc` directory. If you are planning to contribute documentation,
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
[please check our best practices for writing documentation](http://elixir-lang.org/docs/stable/elixir/writing-documentation.html).
## Development links
+23 -9
View File
@@ -10,26 +10,40 @@ This document simply outlines the release process:
3. Ensure CHANGELOG is updated and add current date
4. If a new `vMAJOR.MINOR`, replace "master" with "vVERSION" in the "Deprecations" page and commit
4. Commit changes above with title "Release vVERSION" and generate new tag
5. If a new `vMAJOR.MINOR`, create a new branch "vMAJOR.MINOR" and set `CANONICAL=` in Makefile
5. Run `make clean test` to ensure all tests pass from scratch and the CI is green
6. Commit changes above with title "Release vVERSION" and generate new tag
6. Recompile an existing project (for example, Ecto) to ensure manifests can be upgraded
7. Run `make clean test` to ensure all tests pass from scratch and the CI is green
7. Push branch and the new tag
8. Recompile an existing project (for example, Ecto) to ensure manifests can be upgraded
8. Publish new docs with `make publish_docs`, copy docs to `docs/stable` if appropriate, and push to GitHub Pages
9. Push branch and the new tag
9. Publish new zips with `make publish_zips`, upload `Precompiled.zip` and `Docs.zip` to GitHub Releases
10. Publish new zips with `make zips`, upload `Precompiled.zip` and `Docs.zip` to GitHub Releases
10. Add the release to `elixir.csv` file in `elixir-lang/elixir-lang.github.com`
11. Add the release to `elixir.csv` and `_data/elixir-versions.yml` files in `elixir-lang/elixir-lang.github.com`
## New vMAJOR.MINOR releases
12. After a new `vMAJOR.MINOR`, move back to master, bump versions, start new CHANGELOG, add `-dev` back and commit "Start vMAJOR.MINOR+1"
11. Create a new branch "vMAJOR.MINOR"
12. Move docs generation to `docs/vMAJOR.MINOR` in Makefile, set CANONICAL to stable and copy them to `docs/stable` (change index.html accordingly)
13. In master, bump versions, start new CHANGELOG, add `-dev` back and commit "Start vVERSION+1"
## Places where version is mentioned
* VERSION
* CHANGELOG.md
* src/elixir.app.src (not lib/elixir/src/elixir.app.src)
## Deprecation policy
Elixir deprecations happens in 3 steps:
1. The feature is soft-deprecated. It means both CHANGELOG and documentation must list the feature as deprecated but no warning is effectively emitted by running the code. There is no requirement to soft-deprecate a feature.
2. The feature is effectively deprecated by emitting warnings on usage. In order to deprecate a feature, the proposed alternative MUST exist for AT LEAST two versions. For example, `Enum.uniq/2` was soft-deprecated in favor of `Enum.uniq_by/2` in Elixir v1.1. This means a deprecation warning may only be emitted by Elixir v1.3 or later.
3. The feature is removed. This can only happen on major releases. This means deprecated features in Elixir v1.x shall only be removed by Elixir v2.x.
+1 -1
View File
@@ -1 +1 @@
1.5.2
1.3.4
+3 -4
View File
@@ -2,6 +2,7 @@
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
echo "Usage: `basename $0` [options] [.exs file] [data]
-v Prints version and exits
-e COMMAND Evaluates the given command (*)
-r FILE Requires the given files/patterns (*)
-S SCRIPT   Finds and executes the given script in PATH
@@ -13,14 +14,12 @@ if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
--cookie COOKIE Sets a cookie for this distributed node
--detached Starts the Erlang VM detached from console
--erl SWITCHES Switches to be passed down to Erlang (*)
--help, -h Prints this message and exits
--hidden Makes a hidden node
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--name NAME Makes and assigns a name to the distributed node
--no-halt Does not halt the Erlang VM after execution
--sname NAME Makes and assigns a short name to the distributed node
--version, -v Prints Elixir version and exits
--werl Uses Erlang's Windows shell GUI (Windows only)
** Options marked with (*) can be given more than once
@@ -75,14 +74,14 @@ while [ $I -le $# ]; do
--logger-otp-reports)
I=$(expr $I + 1)
eval "VAL=\${$I}"
if [ "$VAL" = 'true' ] || [ "$VAL" = 'false' ]; then
if [ "$VAL" == 'true' ] || [ "$VAL" == 'false' ]; then
ERL="$ERL -logger handle_otp_reports "$VAL""
fi
;;
--logger-sasl-reports)
I=$(expr $I + 1)
eval "VAL=\${$I}"
if [ "$VAL" = 'true' ] || [ "$VAL" = 'false' ]; then
if [ "$VAL" == 'true' ] || [ "$VAL" == 'false' ]; then
ERL="$ERL -logger handle_sasl_reports "$VAL""
fi
;;
+31 -33
View File
@@ -1,15 +1,16 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
if ""%1""=="""" goto documentation
if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation
if /I ""%1""==""/h"" goto documentation
if ""%1""==""/?"" goto documentation
if ""%1""=="""" goto :documentation
if /I ""%1""==""--help"" goto :documentation
if /I ""%1""==""-h"" goto :documentation
if /I ""%1""==""/h"" goto :documentation
if ""%1""==""/?"" goto :documentation
goto parseopts
:documentation
echo Usage: %~nx0 [options] [.exs file] [data]
echo.
echo -v Prints version and exits
echo -e COMMAND Evaluates the given command (*)
echo -r FILE Requires the given files/patterns (*)
echo -S SCRIPT Finds and executes the given script in PATH
@@ -21,14 +22,12 @@ echo --app APP Starts the given app and its dependencies (*)
echo --cookie COOKIE Sets a cookie for this distributed node
echo --detached Starts the Erlang VM detached from console
echo --erl SWITCHES Switches to be passed down to Erlang (*)
echo --help, -h Prints this message and exits
echo --hidden Makes a hidden node
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --name NAME Makes and assigns a name to the distributed node
echo --no-halt Does not halt the Erlang VM after execution
echo --sname NAME Makes and assigns a short name to the distributed node
echo --version, -v Prints Elixir version and exits
echo --werl Uses Erlang's Windows shell GUI
echo.
echo ** Options marked with (*) can be given more than once
@@ -62,52 +61,51 @@ set par="%1"
shift
if "%par%"=="" (
rem if no parameters defined
goto expand_erl_libs
goto :expand_erl_libs
)
if "%par%"=="""" (
rem if no parameters defined - special case for parameter that is already quoted
goto expand_erl_libs
goto :expand_erl_libs
)
rem ******* EXECUTION OPTIONS **********************
if "%par%"==""--werl"" (set useWerl=1)
if "%par%"==""+iex"" (set runMode="iex")
IF "%par%"==""--werl"" (Set useWerl=1)
IF "%par%"==""+iex"" (Set runMode="iex")
rem ******* ELIXIR PARAMETERS **********************
rem Note: we don't have to do anything with options that don't take an argument
if """"=="%par:-e=%" (shift)
if """"=="%par:-r=%" (shift)
if """"=="%par:-pr=%" (shift)
if """"=="%par:-pa=%" (shift)
if """"=="%par:-pz=%" (shift)
if """"=="%par:--app=%" (shift)
if """"=="%par:--remsh=%" (shift)
IF """"=="%par:-e=%" (shift)
IF """"=="%par:-r=%" (shift)
IF """"=="%par:-pr=%" (shift)
IF """"=="%par:-pa=%" (shift)
IF """"=="%par:-pz=%" (shift)
IF """"=="%par:--app=%" (shift)
IF """"=="%par:--remsh=%" (shift)
rem ******* ERLANG PARAMETERS **********************
if """"=="%par:--detached=%" (set parsErlang=%parsErlang% -detached)
if """"=="%par:--hidden=%" (set parsErlang=%parsErlang% -hidden)
if """"=="%par:--cookie=%" (set parsErlang=%parsErlang% -setcookie %1 && shift)
if """"=="%par:--sname=%" (set parsErlang=%parsErlang% -sname %1 && shift)
if """"=="%par:--name=%" (set parsErlang=%parsErlang% -name %1 && shift)
if """"=="%par:--logger-otp-reports=%" (set parsErlang=%parsErlang% -logger handle_otp_reports %1 && shift)
if """"=="%par:--logger-sasl-reports=%" (set parsErlang=%parsErlang% -logger handle_sasl_reports %1 && shift)
if """"=="%par:--erl=%" (set beforeExtra=%beforeExtra% %~1 && shift)
IF """"=="%par:--detached=%" (Set parsErlang=%parsErlang% -detached)
IF """"=="%par:--hidden=%" (Set parsErlang=%parsErlang% -hidden)
IF """"=="%par:--cookie=%" (Set parsErlang=%parsErlang% -setcookie %1 && shift)
IF """"=="%par:--sname=%" (Set parsErlang=%parsErlang% -sname %1 && shift)
IF """"=="%par:--name=%" (Set parsErlang=%parsErlang% -name %1 && shift)
IF """"=="%par:--logger-otp-reports=%" (Set parsErlang=%parsErlang% -logger handle_otp_reports %1 && shift)
IF """"=="%par:--logger-sasl-reports=%" (Set parsErlang=%parsErlang% -logger handle_sasl_reports %1 && shift)
IF """"=="%par:--erl=%" (Set beforeExtra=%beforeExtra% %~1 && shift)
goto:startloop
rem ******* assume all pre-params are parsed ********************
:expand_erl_libs
rem ******* expand all ebin paths as Windows does not support the ..\*\ebin wildcard ********************
setlocal enabledelayedexpansion
SETLOCAL enabledelayedexpansion
set ext_libs=
for /d %%d in ("%originPath%..\lib\*.") do (
set ext_libs=!ext_libs! -pa "%%~fd\ebin"
)
setlocal disabledelayedexpansion
SETLOCAL disabledelayedexpansion
:run
if not %runMode% == "iex" (
IF NOT %runMode% == "iex" (
set beforeExtra=-noshell -s elixir start_cli %beforeExtra%
)
if %useWerl% equ 1 (
IF %useWerl% EQU 1 (
start werl.exe %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
) else (
) ELSE (
erl.exe %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
)
:end
+6 -9
View File
@@ -2,15 +2,12 @@
if [ $# -eq 0 ] || [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
echo "Usage: `basename $0` [elixir switches] [compiler switches] [.ex files]
-o The directory to output compiled files
--help, -h Prints this message and exits
--ignore-module-conflict Does not emit warnings if a module was previously defined
--no-debug-info Does not attach debug info to compiled modules
--no-docs Does not attach documentation to compiled modules
--verbose Prints compilation status
--version, -v Prints Elixir version and exits
--warnings-as-errors Treats warnings as errors and return non-zero exit code
-o The directory to output compiled files
--no-docs Do not attach documentation to compiled modules
--no-debug-info Do not attach debug info to compiled modules
--verbose Print compilation status
--warnings-as-errors Treat warnings as errors and return non-zero exit code
--ignore-module-conflict
** Options given after -- are passed down to the executed code
** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS
+12 -15
View File
@@ -1,12 +1,12 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
set argc=0
for %%A in (%*) do (
if /I "%%A"=="--help" goto documentation
if /I "%%A"=="-h" goto documentation
if /I "%%A"=="/h" goto documentation
if "%%A"=="/?" goto documentation
set /A argc+=1
if /I "%%A"=="--help" goto documentation
if /I "%%A"=="-h" goto documentation
if /I "%%A"=="/h" goto documentation
if "%%A"=="/?" goto documentation
set /A argc+=1
)
if %argc%==0 goto documentation
goto run
@@ -14,15 +14,12 @@ goto run
:documentation
echo Usage: %~nx0 [elixir switches] [compiler switches] [.ex files]
echo.
echo -o The directory to output compiled files
echo.
echo --help, -h Prints this message and exits
echo --ignore-module-conflict Does not emit warnings if a module was previously defined
echo --no-debug-info Does not attach debug info to compiled modules
echo --no-docs Does not attach documentation to compiled modules
echo --verbose Prints compilation status
echo --version, -v Prints Elixir version and exits
echo --warnings-as-errors Treats warnings as errors and returns non-zero exit code
echo -o The directory to output compiled files
echo --no-docs Do not attach documentation to compiled modules
echo --no-debug-info Do not attach debug info to compiled modules
echo --verbose Print compilation status
echo --warnings-as-errors Treat warnings as errors and return non-zero exit code
echo --ignore-module-conflict
echo.
echo ** Options given after -- are passed down to the executed code
echo ** Options can be passed to the Erlang runtime using ELIXIR_ERL_OPTIONS
+2 -3
View File
@@ -2,6 +2,7 @@
if [ $# -gt 0 ] && ([ "$1" = "--help" ] || [ "$1" = "-h" ]); then
echo "Usage: `basename $0` [options] [.exs file] [data]
-v Prints version and exits
-e COMMAND Evaluates the given command (*)
-r FILE Requires the given files/patterns (*)
-S SCRIPT   Finds and executes the given script in PATH
@@ -13,14 +14,12 @@ if [ $# -gt 0 ] && ([ "$1" = "--help" ] || [ "$1" = "-h" ]); then
--cookie COOKIE Sets a cookie for this distributed node
--detached Starts the Erlang VM detached from console
--erl SWITCHES Switches to be passed down to Erlang (*)
--help, -h Prints this message and exits
--hidden Makes a hidden node
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--name NAME Makes and assigns a name to the distributed node
--no-halt Does not halt the Erlang VM after execution
--sname NAME Makes and assigns a short name to the distributed node
--version, -v Prints IEx version and exits
--werl Uses Erlang's Windows shell GUI (Windows only)
--dot-iex PATH Overrides default .iex.exs file and uses path instead;
@@ -45,4 +44,4 @@ readlink_f () {
SELF=$(readlink_f "$0")
SCRIPT_PATH=$(dirname "$SELF")
exec "$SCRIPT_PATH"/elixir --no-halt --erl "-noshell -user Elixir.IEx.CLI" +iex "$@"
exec "$SCRIPT_PATH"/elixir --no-halt --erl "-user Elixir.IEx.CLI" +iex "$@"
+24 -25
View File
@@ -1,5 +1,5 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
SETLOCAL
if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation
if /I ""%1""==""/h"" goto documentation
@@ -9,30 +9,29 @@ goto run
:documentation
echo Usage: %~nx0 [options] [.exs file] [data]
echo.
echo -e COMMAND Evaluates the given command (*)
echo -r FILE Requires the given files/patterns (*)
echo -S SCRIPT Finds and executes the given script in PATH
echo -pr FILE Requires the given files/patterns in parallel (*)
echo -pa PATH Prepends the given path to Erlang code path (*)
echo -pz PATH Appends the given path to Erlang code path (*)
echo -v Prints version and exits
echo -e COMMAND Evaluates the given command (*)
echo -r FILE Requires the given files/patterns (*)
echo -S SCRIPT Finds and executes the given script in PATH
echo -pr FILE Requires the given files/patterns in parallel (*)
echo -pa PATH Prepends the given path to Erlang code path (*)
echo -pz PATH Appends the given path to Erlang code path (*)
echo.
echo --app APP Starts the given app and its dependencies (*)
echo --cookie COOKIE Sets a cookie for this distributed node
echo --detached Starts the Erlang VM detached from console
echo --erl SWITCHES Switches to be passed down to Erlang (*)
echo --help, -h Prints this message and exits
echo --hidden Makes a hidden node
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --name NAME Makes and assigns a name to the distributed node
echo --no-halt Does not halt the Erlang VM after execution
echo --sname NAME Makes and assigns a short name to the distributed node
echo --version, -v Prints IEx version and exits
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo --app APP Starts the given app and its dependencies (*)
echo --cookie COOKIE Sets a cookie for this distributed node
echo --detached Starts the Erlang VM detached from console
echo --erl SWITCHES Switches to be passed down to Erlang (*)
echo --hidden Makes a hidden node
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --name NAME Makes and assigns a name to the distributed node
echo --no-halt Does not halt the Erlang VM after execution
echo --sname NAME Makes and assigns a short name to the distributed node
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo --dot-iex PATH Overrides default .iex.exs file and uses path instead;
echo path can be empty, then no file will be loaded
echo --remsh NAME Connects to a node using a remote shell
echo --dot-iex PATH Overrides default .iex.exs file and uses path instead;
echo path can be empty, then no file will be loaded
echo --remsh NAME Connects to a node using a remote shell
echo.
echo ** Options marked with (*) can be given more than once
echo ** Options given after the .exs file or -- are passed down to the executed code
@@ -41,6 +40,6 @@ goto end
:run
@if defined IEX_WITH_WERL (@set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" --no-halt --erl "-noshell -user Elixir.IEx.CLI" +iex %__ELIXIR_IEX_FLAGS% %*
call "%~dp0\elixir.bat" +iex --erl "-user Elixir.IEx.CLI" --no-halt %__ELIXIR_IEX_FLAGS% %*
:end
endlocal
ENDLOCAL
+2 -2
View File
@@ -1,2 +1,2 @@
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
+6 -12
View File
@@ -111,7 +111,7 @@ defmodule EEx do
"""
defmacro function_from_string(kind, name, source, args \\ [], options \\ []) do
quote bind_quoted: binding() do
quote bind_quoted: binding do
info = Keyword.merge [file: __ENV__.file, line: __ENV__.line], options
args = Enum.map args, fn arg -> {arg, [line: info[:line]], nil} end
compiled = EEx.compile_string(source, info)
@@ -148,7 +148,7 @@ defmodule EEx do
"""
defmacro function_from_file(kind, name, file, args \\ [], options \\ []) do
quote bind_quoted: binding() do
quote bind_quoted: binding do
info = Keyword.merge options, [file: file, line: 1]
args = Enum.map args, fn arg -> {arg, [line: 1], nil} end
compiled = EEx.compile_file(file, info)
@@ -166,8 +166,7 @@ defmodule EEx do
Gets a string `source` and generate a quoted expression
that can be evaluated by Elixir or compiled to a function.
"""
@spec compile_string(String.t, keyword) :: Macro.t | no_return
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
def compile_string(source, options \\ []) do
EEx.Compiler.compile(source, options)
end
@@ -175,8 +174,7 @@ defmodule EEx do
Gets a `filename` and generate a quoted expression
that can be evaluated by Elixir or compiled to a function.
"""
@spec compile_file(String.t, keyword) :: Macro.t | no_return
def compile_file(filename, options \\ []) when is_binary(filename) and is_list(options) do
def compile_file(filename, options \\ []) do
options = Keyword.merge options, [file: filename, line: 1]
compile_string(File.read!(filename), options)
end
@@ -190,9 +188,7 @@ defmodule EEx do
"foo baz"
"""
@spec eval_string(String.t, keyword, keyword) :: any
def eval_string(source, bindings \\ [], options \\ [])
when is_binary(source) and is_list(bindings) and is_list(options) do
def eval_string(source, bindings \\ [], options \\ []) do
compiled = compile_string(source, options)
do_eval(compiled, bindings, options)
end
@@ -209,9 +205,7 @@ defmodule EEx do
EEx.eval_file "sample.eex", [bar: "baz"] #=> "foo baz"
"""
@spec eval_file(String.t, keyword, keyword) :: any
def eval_file(filename, bindings \\ [], options \\ [])
when is_binary(filename) and is_list(bindings) and is_list(options) do
def eval_file(filename, bindings \\ [], options \\ []) do
options = Keyword.put options, :file, filename
compiled = compile_file(filename, options)
do_eval(compiled, bindings, options)
+27 -52
View File
@@ -9,78 +9,55 @@ defmodule EEx.Compiler do
and the engine together by handling the tokens and invoking
the engine every time a full expression or text is received.
"""
@spec compile(String.t, keyword) :: Macro.t | no_return
def compile(source, opts) when is_binary(source) and is_list(opts) do
file = opts[:file] || "nofile"
line = opts[:line] || 1
trim = opts[:trim] || false
def compile(source, opts) do
file = opts[:file] || "nofile"
line = opts[:line] || 1
trim = opts[:trim] || false
case EEx.Tokenizer.tokenize(source, line, trim: trim) do
{:ok, tokens} ->
state = %{engine: opts[:engine] || @default_engine,
file: file, line: line, quoted: [], start_line: nil}
init = state.engine.init(opts)
generate_buffer(tokens, init, [], state)
generate_buffer(tokens, state.engine.init(opts), [], state)
{:error, line, message} ->
raise EEx.SyntaxError, line: line, file: file, message: message
end
end
# Generates the buffers by handling each expression from the tokenizer.
# It returns Macro.t/0 or it raises.
# Generates the buffers by handling each expression from the tokenizer
defp generate_buffer([{:text, chars} | rest], buffer, scope, state) do
defp generate_buffer([{:text, chars} | t], buffer, scope, state) do
buffer = state.engine.handle_text(buffer, IO.chardata_to_string(chars))
generate_buffer(rest, buffer, scope, state)
generate_buffer(t, buffer, scope, state)
end
defp generate_buffer([{:expr, line, mark, chars} | rest], buffer, scope, state) do
defp generate_buffer([{:expr, line, mark, chars} | t], buffer, scope, state) do
expr = Code.string_to_quoted!(chars, [line: line, file: state.file])
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
generate_buffer(rest, buffer, scope, state)
generate_buffer(t, buffer, scope, state)
end
defp generate_buffer([{:start_expr, start_line, mark, chars} | rest], buffer, scope, state) do
{contents, line, rest} = look_ahead_text(rest, start_line, chars)
{contents, rest} =
generate_buffer(rest, state.engine.handle_begin(buffer), [contents | scope],
%{state | quoted: [], line: line, start_line: start_line})
defp generate_buffer([{:start_expr, start_line, mark, chars} | t], buffer, scope, state) do
{contents, line, t} = look_ahead_text(t, start_line, chars)
{contents, t} = generate_buffer(t, "", [contents | scope],
%{state | quoted: [], line: line, start_line: start_line})
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state)
generate_buffer(t, buffer, scope, state)
end
defp generate_buffer([{:middle_expr, line, '', chars} | rest], buffer, [current | scope], state) do
defp generate_buffer([{:middle_expr, line, _, chars} | t], buffer, [current | scope], state) do
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
generate_buffer(rest, state.engine.handle_begin(buffer), [wrapped | scope], %{state | line: line})
generate_buffer(t, "", [wrapped | scope], %{state | line: line})
end
defp generate_buffer([{:middle_expr, line, modifier, chars} | t], buffer, scope, state) do
message = "unexpected beginning of EEx tag \"<%#{modifier}\" on \"<%#{modifier}#{chars}%>\", " <>
"please remove \"#{modifier}\" accordingly"
:elixir_errors.warn line, state.file, message
generate_buffer([{:middle_expr, line, '', chars} | t], buffer, scope, state)
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
# raise EEx.SyntaxError, message: message, file: state.file, line: line
end
defp generate_buffer([{:end_expr, line, '', chars} | rest], buffer, [current | _], state) do
defp generate_buffer([{:end_expr, line, _, chars} | t], buffer, [current | _], state) do
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
tuples = Code.string_to_quoted!(wrapped, [line: state.start_line, file: state.file])
buffer = insert_quoted(tuples, state.quoted)
{buffer, rest}
end
defp generate_buffer([{:end_expr, line, modifier, chars} | t], buffer, [_ | _] = scope, state) do
message = "unexpected beginning of EEx tag \"<%#{modifier}\" on end of expression \"<%#{modifier}#{chars}%>\", " <>
"please remove \"#{modifier}\" accordingly"
:elixir_errors.warn line, state.file, message
generate_buffer([{:end_expr, line, '', chars} | t], buffer, scope, state)
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
# raise EEx.SyntaxError, message: message, file: state.file, line: line
{buffer, t}
end
defp generate_buffer([{:end_expr, line, _, chars} | _], _buffer, [], state) do
raise EEx.SyntaxError, message: "unexpected end of expression <%#{chars}%>",
file: state.file, line: line
raise EEx.SyntaxError, message: "unexpected token #{inspect chars}", file: state.file, line: line
end
defp generate_buffer([], buffer, [], state) do
@@ -99,23 +76,21 @@ defmodule EEx.Compiler do
key = length(state.quoted)
placeholder = '__EEX__(' ++ Integer.to_charlist(key) ++ ');'
{current ++ placeholder ++ new_lines ++ chars,
%{state | quoted: [{key, state.engine.handle_end(buffer)} | state.quoted]}}
%{state | quoted: [{key, buffer} | state.quoted]}}
end
# Look text ahead on expressions
defp look_ahead_text([{:text, text}, {:middle_expr, line, _, chars} | rest] = tokens, start, contents) do
defp look_ahead_text([{:text, text}, {:middle_expr, line, _, chars} | t]=list, start, contents) do
if only_spaces?(text) do
{contents ++ text ++ chars, line, rest}
{contents ++ text ++ chars, line, t}
else
{contents, start, tokens}
{contents, start, list}
end
end
defp look_ahead_text([{:middle_expr, line, _, chars} | rest], _start, contents) do
{contents ++ chars, line, rest}
end
defp look_ahead_text(tokens, start, contents) do
{contents, start, tokens}
defp look_ahead_text(t, start, contents) do
{contents, start, t}
end
defp only_spaces?(chars) do
+16 -50
View File
@@ -2,26 +2,19 @@ defmodule EEx.Engine do
@moduledoc ~S"""
Basic EEx engine that ships with Elixir.
An engine needs to implement six functions:
An engine needs to implement three functions:
* `init(opts)` - called at the beginning of every text
and it must return the initial state.
* `init(opts)` - returns the initial buffer
* `handle_body(state)` - receives the state of the document
and it must return a quoted expression.
* `handle_body(quoted)` - receives the final built quoted
expression, should do final post-processing and return a
quoted expression.
* `handle_text(state, text)` - it receives the state,
* `handle_text(buffer, text)` - it receives the buffer,
the text and must return a new quoted expression.
* `handle_expr(state, marker, expr)` - it receives the state,
the marker, the expr and must return a new state.
* `handle_begin(state)` - called every time there a new state
is needed with an empty buffer. Typically called for do/end
blocks, case expressions, anonymous functions, etc
* `handle_end(state)` - opposite of `handle_begin(state)` and
it must return quoted expression
* `handle_expr(buffer, marker, expr)` - it receives the buffer,
the marker, the expr and must return a new quoted expression.
The marker is what follows exactly after `<%`. For example,
`<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
@@ -34,14 +27,10 @@ defmodule EEx.Engine do
default implementations for the functions above.
"""
@type state :: term
@callback init(opts :: keyword) :: state
@callback handle_body(state) :: Macro.t
@callback handle_text(state, text :: String.t) :: state
@callback handle_expr(state, marker :: String.t, expr :: Macro.t) :: state
@callback handle_begin(state) :: state
@callback handle_end(state) :: Macro.t
@callback init(Keyword.t) :: Macro.t
@callback handle_body(Macro.t) :: Macro.t
@callback handle_text(Macro.t, String.t) :: Macro.t
@callback handle_expr(Macro.t, String.t, Macro.t) :: Macro.t
@doc false
defmacro __using__(_) do
@@ -52,16 +41,8 @@ defmodule EEx.Engine do
EEx.Engine.init(opts)
end
def handle_body(quoted) do
EEx.Engine.handle_body(quoted)
end
def handle_begin(quoted) do
EEx.Engine.handle_begin(quoted)
end
def handle_end(quoted) do
EEx.Engine.handle_end(quoted)
def handle_body(body) do
EEx.Engine.handle_body(body)
end
def handle_text(buffer, text) do
@@ -72,7 +53,7 @@ defmodule EEx.Engine do
EEx.Engine.handle_expr(buffer, marker, expr)
end
defoverridable EEx.Engine
defoverridable [handle_body: 1, handle_expr: 3, handle_text: 2, init: 1]
end
end
@@ -91,7 +72,6 @@ defmodule EEx.Engine do
end
"""
@spec handle_assign(Macro.t) :: Macro.t
def handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
line = meta[:line] || 0
quote line: line, do: EEx.Engine.fetch_assign!(var!(assigns), unquote(name))
@@ -102,7 +82,6 @@ defmodule EEx.Engine do
@doc false
# TODO: Raise on 2.0
@spec fetch_assign!(Access.t, Access.key) :: term | nil
def fetch_assign!(assigns, key) do
case Access.fetch(assigns, key) do
{:ok, val} ->
@@ -123,20 +102,6 @@ defmodule EEx.Engine do
""
end
@doc """
Returns an empty string as the new buffer.
"""
def handle_begin(_previous) do
""
end
@doc """
End of the new buffer.
"""
def handle_end(quoted) do
quoted
end
@doc """
The default implementation simply returns the given expression.
"""
@@ -159,6 +124,7 @@ defmodule EEx.Engine do
All other markers are not implemented by this engine.
"""
@spec handle_expr(Macro.t, String.t, Macro.t) :: Macro.t
def handle_expr(buffer, "=", expr) do
quote do
tmp1 = unquote(buffer)
+8 -15
View File
@@ -1,34 +1,26 @@
defmodule EEx.Tokenizer do
@moduledoc false
@type content :: IO.chardata
@type line :: non_neg_integer
@type token :: {:text, content} |
{:expr | :start_expr | :middle_expr | :end_expr, line, '=' | '', content}
@doc """
Tokenizes the given charlist or binary.
It returns {:ok, list} with the following tokens:
* `{:text, content}`
* `{:expr, line, marker, content}`
* `{:start_expr, line, marker, content}`
* `{:middle_expr, line, marker, content}`
* `{:end_expr, line, marker, content}`
* `{:text, contents}`
* `{:expr, line, marker, contents}`
* `{:start_expr, line, marker, contents}`
* `{:middle_expr, line, marker, contents}`
* `{:end_expr, line, marker, contents}`
Or `{:error, line, error}` in case of errors.
"""
@spec tokenize(binary | charlist, line, keyword) :: {:ok, [token]} | {:error, line, String.t}
def tokenize(bin, line, opts \\ [])
def tokenize(bin, line, opts)
when is_binary(bin) and is_integer(line) and line >= 0 and is_list(opts) do
def tokenize(bin, line, opts) when is_binary(bin) do
tokenize(String.to_charlist(bin), line, opts)
end
def tokenize(list, line, opts)
when is_list(list) and is_integer(line) and line >= 0 and is_list(opts) do
def tokenize(list, line, opts) do
tokenize(list, line, opts, [], [])
end
@@ -181,6 +173,7 @@ defmodule EEx.Tokenizer do
# If trim mode is enabled and the token is on a line with
# only itself and whitespace, trim the whitespace around it,
# including the line break following it if there is one.
defp trim_if_needed(rest, line, opts, buffer, acc) do
original = {rest, line, buffer}
if opts[:trim] do
+3 -5
View File
@@ -2,10 +2,8 @@ defmodule EEx.Mixfile do
use Mix.Project
def project do
[
app: :eex,
version: System.version,
build_per_environment: false
]
[app: :eex,
version: System.version,
build_per_environment: false]
end
end
+6 -5
View File
@@ -1,8 +1,9 @@
Code.require_file "../test_helper.exs", __DIR__
defmodule EEx.SmartEngineTest do
# TODO: Make this async: true once capture_io is removed
use ExUnit.Case
use ExUnit.Case, async: true
import ExUnit.CaptureIO
test "evaluates simple string" do
assert_eval "foo bar", "foo bar"
@@ -17,7 +18,7 @@ defmodule EEx.SmartEngineTest do
end
test "error with missing assigns" do
stderr = ExUnit.CaptureIO.capture_io(:stderr, fn ->
stderr = capture_io(:stderr, fn ->
assert_eval "", "<%= @foo %>", assigns: %{}
end)
assert stderr =~ "assign @foo not available in EEx template"
@@ -27,7 +28,7 @@ defmodule EEx.SmartEngineTest do
assert_eval "1\n2\n3\n", "<%= for x <- [1, 2, 3] do %><%= x %>\n<% end %>"
end
test "preserves line numbers" do
test "compiled preserved line numbers" do
result = EEx.compile_string("<%= @hello %>", engine: EEx.SmartEngine)
Macro.prewalk(result, fn
{_left, meta, _right} ->
@@ -38,7 +39,7 @@ defmodule EEx.SmartEngineTest do
end
defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
result = EEx.eval_string(actual, binding, file: __ENV__.file)
assert result == expected
end
end
+307 -365
View File
@@ -4,7 +4,7 @@ require EEx
defmodule EExTest.Compiled do
def before_compile do
fill_in_stacktrace()
fill_in_stacktrace
{__ENV__.line, hd(tl(System.stacktrace))}
end
@@ -19,13 +19,13 @@ defmodule EExTest.Compiled do
def file_sample(arg), do: private_file_sample(arg)
def after_compile do
fill_in_stacktrace()
fill_in_stacktrace
{__ENV__.line, hd(tl(System.stacktrace))}
end
@file "unknown"
def unknown do
fill_in_stacktrace()
fill_in_stacktrace
{__ENV__.line, hd(tl(System.stacktrace))}
end
@@ -53,422 +53,368 @@ defmodule EExTest do
doctest EEx.Engine
doctest EEx.SmartEngine
describe "evaluates" do
test "simple string" do
assert_eval "foo bar", "foo bar"
end
test "evaluates simple string" do
assert_eval "foo bar", "foo bar"
end
test "Unicode" do
template = """
• <%= "•" %> •
<%= "Jößé Vâlìm" %> Jößé Vâlìm
"""
assert_eval " • • •\n Jößé Vâlìm Jößé Vâlìm\n", template
end
test "evaluates with embedded" do
assert_eval "foo bar", "foo <%= :bar %>"
end
test "trim mode" do
string = "<%= 123 %> \n456\n <%= 789 %>"
expected = "123456\n789"
assert_eval expected, string, [], trim: true
end
test "evaluates with embedded and the binding" do
assert EEx.eval_string("foo <%= bar %>", [bar: 1]) == "foo 1"
end
test "trim mode with middle expression" do
string = """
<%= cond do %>
<% false -> %>
this
<% true -> %>
that
<% end %>
"""
expected = " that\n"
assert_eval expected, string, [], trim: true
end
test "evaluates with embedded do end" do
assert_eval "foo bar", "foo <%= if true do %>bar<% end %>"
end
test "embedded code" do
assert_eval "foo bar", "foo <%= :bar %>"
end
test "evaluates with embedded do end and eval the expression" do
assert_eval "foo ", "foo <%= if false do %>bar<% end %>"
end
test "embedded code with binding" do
assert EEx.eval_string("foo <%= bar %>", [bar: 1]) == "foo 1"
end
test "evaluates with embedded do end and nested print expression" do
assert_eval "foo bar", "foo <%= if true do %><%= :bar %><% end %>"
end
test "embedded code with do end when true" do
assert_eval "foo bar", "foo <%= if true do %>bar<% end %>"
end
test "evaluates with embedded do end and nested expressions" do
assert_eval "foo bar baz", "foo <%= if true do %>bar <% Process.put(:eex_text, 1) %><%= :baz %><% end %>"
assert Process.get(:eex_text) == 1
end
test "embedded code with do end when false" do
assert_eval "foo ", "foo <%= if false do %>bar<% end %>"
end
test "evaluates with embedded middle expression" do
assert_eval "foo bar", "foo <%= if true do %>bar<% else %>baz<% end %>"
end
test "embedded code with do end and expression" do
assert_eval "foo bar", "foo <%= if true do %><%= :bar %><% end %>"
end
test "evaluates with embedded middle expression and eval the expression" do
assert_eval "foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>"
end
test "embedded code with do end and multiple expressions" do
assert_eval "foo bar baz", "foo <%= if true do %>bar <% Process.put(:eex_text, 1) %><%= :baz %><% end %>"
assert Process.get(:eex_text) == 1
end
test "evaluates with nested start expression" do
assert_eval "foo bar", "foo <%= if true do %><%= if true do %>bar<% end %><% end %>"
end
test "embedded code with middle expression" do
assert_eval "foo bar", "foo <%= if true do %>bar<% else %>baz<% end %>"
end
test "evaluates with nested middle expression" do
assert_eval "foo baz", "foo <%= if true do %><%= if false do %>bar<% else %>baz<% end %><% end %>"
end
test "embedded code with evaluated middle expression" do
assert_eval "foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>"
end
test "evaluates with parentheses after end in end token" do
assert_eval " 101 102 103 ", "<%= Enum.map([1, 2, 3], (fn x -> %> <%= 100 + x %> <% end) ) %>"
end
test "embedded code with nested do end" do
assert_eval "foo bar", "foo <%= if true do %><%= if true do %>bar<% end %><% end %>"
end
test "evaluates with defined variable" do
assert_eval "foo 1", "foo <% bar = 1 %><%= bar %>"
end
test "embedded code with nested do end with middle expression" do
assert_eval "foo baz", "foo <%= if true do %><%= if false do %>bar<% else %>baz<% end %><% end %>"
end
test "evaluates with require code" do
assert_eval "foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>"
end
test "embedded code with parentheses after end in end token" do
assert_eval " 101 102 103 ", "<%= Enum.map([1, 2, 3], (fn x -> %> <%= 100 + x %> <% end) ) %>"
end
test "evaluates with end of token" do
assert_eval "foo bar %>", "foo bar %>"
end
test "embedded code with variable definition" do
assert_eval "foo 1", "foo <% bar = 1 %><%= bar %>"
end
test "embedded code with require" do
assert_eval "foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>"
end
test "with end of token" do
assert_eval "foo bar %>", "foo bar %>"
test "raises a syntax error when the token is invalid" do
assert_raise EEx.SyntaxError, "nofile:1: missing token '%>'", fn ->
EEx.compile_string "foo <%= bar"
end
end
describe "raises syntax errors" do
test "when the token is invalid" do
assert_raise EEx.SyntaxError, "nofile:1: missing token '%>'", fn ->
EEx.compile_string "foo <%= bar"
end
end
test "when end expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
EEx.compile_string "foo <% end %>"
end
end
test "when start expression is found without an end expression" do
assert_raise EEx.SyntaxError, "nofile:2: unexpected end of string, expected a closing '<% end %>'", fn ->
EEx.compile_string "foo\n<% if true do %>"
end
end
test "when nested end expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
EEx.compile_string "foo <% if true do %><% end %><% end %>"
end
end
test "when middle expression has a modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string "foo <%= if true do %>true<%= else %>false<% end %>"
end) =~ ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= else %>\"]
end
test "when end expression has a modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string "foo <%= if true do %>true<% else %>false<%= end %>"
end) =~ ~s[unexpected beginning of EEx tag \"<%=\" on end of expression \"<%= end %>\"]
test "raises a syntax error when end expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1: unexpected token ' end '", fn ->
EEx.compile_string "foo <% end %>"
end
end
describe "environment" do
test "respects line numbers" do
expected = """
foo
2
"""
string = """
foo
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects line numbers inside nested expressions" do
expected = """
foo
3
5
"""
string = """
foo
<%= if true do %>
<%= __ENV__.line %>
<% end %>
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects line numbers inside start expression" do
expected = """
foo
true
5
"""
string = """
foo
<%= if __ENV__.line == 2 do %>
<%= true %>
<% end %>
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects line numbers inside middle expression with ->" do
expected = """
foo
true
7
"""
string = """
foo
<%= cond do %>
<% false -> %> false
<% __ENV__.line == 4 -> %>
<%= true %>
<% end %>
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects line number inside middle expressions with keywords" do
expected = """
foo
5
7
"""
string = """
foo
<%= if false do %>
<%= __ENV__.line %>
<% else %>
<%= __ENV__.line %>
<% end %>
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects files" do
assert_eval "sample.ex", "<%= __ENV__.file %>", [], file: "sample.ex"
test "raises a syntax error when start expression is found without an end expression" do
assert_raise EEx.SyntaxError, "nofile:2: unexpected end of string, expected a closing '<% end %>'", fn ->
EEx.compile_string "foo\n<% if true do %>"
end
end
describe "clauses" do
test "inside functions" do
expected = """
Number 1
Number 2
Number 3
"""
string = """
<%= Enum.map [1, 2, 3], fn x -> %>
Number <%= x %>
<% end %>
"""
assert_eval expected, string
end
test "inside cond" do
expected = """
foo
true
"""
string = """
foo
<%= cond do %>
<% false -> %> false
<% fn -> 1 end -> %>
<%= true %>
<% end %>
"""
assert_eval expected, string
end
test "inside cond with do end" do
string = """
<% y = ["a", "b", "c"] %>
<%= cond do %>
<% "a" in y -> %>
Good
<% true -> %>
<% if true do %>true<% else %>false<% end %>
Bad
<% end %>
"""
assert_eval "\n\n Good\n \n", string
test "raises a syntax error when nested end expression is found without a start expression" do
assert_raise EEx.SyntaxError, "nofile:1: unexpected token ' end '", fn ->
EEx.compile_string "foo <% if true do %><% end %><% end %>"
end
end
describe "buffers" do
test "unused buffers are kept out" do
string = """
<%= 123 %>
<% if true do %>
<%= 456 %>
<% end %>
<%= 789 %>
"""
test "respects line numbers" do
expected = """
foo
2
"""
assert_eval "123\n\n789\n", string
end
string = """
foo
<%= __ENV__.line %>
"""
test "inside comprehensions" do
string = """
<%= for _name <- packages || [] do %>
<% end %>
<%= all || :done %>
"""
assert_eval "\ndone\n", string, packages: nil, all: nil
assert_eval expected, string
end
test "respects line numbers inside nested expressions" do
expected = """
foo
3
5
"""
string = """
foo
<%= if true do %>
<%= __ENV__.line %>
<% end %>
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects line numbers inside start expression" do
expected = """
foo
true
5
"""
string = """
foo
<%= if __ENV__.line == 2 do %>
<%= true %>
<% end %>
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects line numbers inside middle expression with ->" do
expected = """
foo
true
7
"""
string = """
foo
<%= cond do %>
<% false -> %> false
<% __ENV__.line == 4 -> %>
<%= true %>
<% end %>
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects line number inside middle expressions with keywords" do
expected = """
foo
5
7
"""
string = """
foo
<%= if false do %>
<%= __ENV__.line %>
<% else %>
<%= __ENV__.line %>
<% end %>
<%= __ENV__.line %>
"""
assert_eval expected, string
end
test "respects files" do
assert_eval "sample.ex", "<%= __ENV__.file %>", [], file: "sample.ex"
end
test "properly handle functions" do
expected = """
Number 1
Number 2
Number 3
"""
string = """
<%= Enum.map [1, 2, 3], fn x -> %>
Number <%= x %>
<% end %>
"""
assert_eval expected, string
end
test "properly handle functions on the left side of clauses" do
expected = """
foo
true
"""
string = """
foo
<%= cond do %>
<% false -> %> false
<% fn -> 1 end -> %>
<%= true %>
<% end %>
"""
assert_eval expected, string
end
test "evaluates nested do expressions" do
string = """
<% y = ["a", "b", "c"] %>
<%= cond do %>
<% "a" in y -> %>
Good
<% true -> %>
<% if true do %>true<% else %>false<% end %>
Bad
<% end %>
"""
assert_eval "\n\n Good\n \n", string
end
test "evaluates expressions with buffers" do
string = """
<%= 123 %>
<% if true do %>
<%= 456 %>
<% end %>
<%= 789 %>
"""
assert_eval "123\n\n789\n", string
end
test "for comprehensions" do
string = """
<%= for _name <- packages || [] do %>
<% end %>
<%= all || :done %>
"""
assert_eval "\ndone\n", string, packages: nil, all: nil
end
test "Unicode" do
template = """
• <%= "•" %> •
<%= "Jößé Vâlìm" %> Jößé Vâlìm
"""
result = EEx.eval_string(template)
assert result == " • • •\n Jößé Vâlìm Jößé Vâlìm\n"
end
test "trim mode" do
string = "<%= 123 %> \n456\n <%= 789 %>"
expected = "123456\n789"
assert_eval expected, string, [], trim: true
end
test "evaluates the source from a given file" do
filename = Path.join(__DIR__, "fixtures/eex_template.eex")
result = EEx.eval_file(filename)
assert result == "foo bar.\n"
end
test "evaluates the source from a given file with bindings" do
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
result = EEx.eval_file(filename, [bar: 1])
assert result == "foo 1\n"
end
test "raises an Exception when there's an error with the given file" do
assert_raise File.Error, "could not read file \"non-existent.eex\": no such file or directory", fn ->
filename = "non-existent.eex"
EEx.compile_file(filename)
end
end
describe "from file" do
test "evaluates the source" do
filename = Path.join(__DIR__, "fixtures/eex_template.eex")
result = EEx.eval_file(filename)
assert_normalized_newline_equal "foo bar.\n", result
end
test "evaluates the source with bindings" do
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
result = EEx.eval_file(filename, [bar: 1])
assert_normalized_newline_equal "foo 1\n", result
end
test "raises an Exception when file is missing" do
assert_raise File.Error, "could not read file \"non-existent.eex\": no such file or directory", fn ->
filename = "non-existent.eex"
EEx.compile_file(filename)
end
end
test "sets external resource attribute" do
assert EExTest.Compiled.__info__(:attributes)[:external_resource] ==
[Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")]
end
test "sets external resource attribute" do
assert EExTest.Compiled.__info__(:attributes)[:external_resource] ==
[Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")]
end
describe "precompiled" do
test "defined from string" do
assert EExTest.Compiled.string_sample(1, 2) == "3"
end
test "from string" do
assert EExTest.Compiled.string_sample(1, 2) == "3"
end
test "defined from file" do
assert EExTest.Compiled.file_sample(1) == "foo 1\n"
assert EExTest.Compiled.public_file_sample(1) == "foo 1\n"
end
test "from file" do
assert_normalized_newline_equal "foo 1\n", EExTest.Compiled.file_sample(1)
assert_normalized_newline_equal "foo 1\n", EExTest.Compiled.public_file_sample(1)
end
test "from file does not affect backtrace" do
assert EExTest.Compiled.before_compile ==
{8,
{EExTest.Compiled,
:before_compile,
0,
[file: to_charlist(Path.relative_to_cwd(__ENV__.file)), line: 7]
}
test "defined from file do not affect backtrace" do
assert EExTest.Compiled.before_compile ==
{8,
{EExTest.Compiled,
:before_compile,
0,
[file: to_charlist(Path.relative_to_cwd(__ENV__.file)), line: 7]
}
}
assert EExTest.Compiled.after_compile ==
{23,
{EExTest.Compiled,
:after_compile,
0,
[file: to_charlist(Path.relative_to_cwd(__ENV__.file)), line: 22]
}
assert EExTest.Compiled.after_compile ==
{23,
{EExTest.Compiled,
:after_compile,
0,
[file: to_charlist(Path.relative_to_cwd(__ENV__.file)), line: 22]
}
}
assert EExTest.Compiled.unknown ==
{29,
{EExTest.Compiled,
:unknown,
0,
[file: 'unknown', line: 28]
}
assert EExTest.Compiled.unknown ==
{29,
{EExTest.Compiled,
:unknown,
0,
[file: 'unknown', line: 28]
}
end
}
end
defmodule TestEngine do
@behaviour EEx.Engine
def init(_opts) do
"INIT"
""
end
def handle_body(body) do
"BODY(#{body})"
end
def handle_begin(_) do
"BEGIN"
end
def handle_end(buffer) do
buffer <> ":END"
{:wrapped, body}
end
def handle_text(buffer, text) do
buffer <> ":TEXT(#{String.trim(text)})"
EEx.Engine.handle_text(buffer, text)
end
def handle_expr(buffer, "=", expr) do
buffer <> ":EQUAL(#{Macro.to_string(expr)})"
def handle_expr(buffer, mark, expr) do
EEx.Engine.handle_expr(buffer, mark, expr)
end
end
describe "custom engines" do
test "text" do
assert_eval "BODY(INIT:TEXT(foo))", "foo", [], engine: TestEngine
end
test "begin/end" do
assert_eval ~s[BODY(INIT:TEXT(foo):EQUAL(if() do\n "BEGIN:TEXT(this):END"\nelse\n "BEGIN:TEXT(that):END"\nend))],
"foo <%= if do %>this<% else %>that<% end %>", [], engine: TestEngine
end
test "calls handle_body" do
assert {:wrapped, "foo"} = EEx.eval_string("foo", [], engine: TestEngine)
end
defp assert_eval(expected, actual, binding \\ [], opts \\ []) do
@@ -476,8 +422,4 @@ defmodule EExTest do
result = EEx.eval_string(actual, binding, opts)
assert result == expected
end
defp assert_normalized_newline_equal(expected, actual) do
assert String.replace(expected, "\r\n", "\n") == String.replace(actual, "\r\n", "\n")
end
end
+86 -291
View File
@@ -17,7 +17,7 @@ defmodule Access do
## Dynamic lookups
Out of the box, `Access` works with `Keyword` and `Map`:
Out of the box, Access works with `Keyword` and `Map`:
iex> keywords = [a: 1, b: 2]
iex> keywords[:a]
@@ -31,10 +31,7 @@ defmodule Access do
iex> star_ratings[1.5]
"★☆"
Note that the dynamic lookup syntax (`term[key]`) roughly translates to
`Access.get(term, key, nil)`.
`Access` can be combined with `Kernel.put_in/3` to put a value
Access can be combined with `Kernel.put_in/3` to put a value
in a given key:
iex> map = %{a: 1, b: 2}
@@ -47,50 +44,47 @@ defmodule Access do
iex> put_in users["john"][:age], 28
%{"john" => %{age: 28}, "meg" => %{age: 23}}
Furthermore, `Access` transparently ignores `nil` values:
Furthermore, Access transparently ignores `nil` values:
iex> keywords = [a: 1, b: 2]
iex> keywords[:c][:unknown]
nil
Since `Access` is a behaviour, it can be implemented for key-value
Since Access is a behaviour, it can be implemented to key-value
data structures. The implementation should be added to the
module that defines the struct being accessed. `Access` requires the
module that defines the struct being access. Access requires the
key comparison to be implemented using the `===` operator.
## Static lookups
The `Access` syntax (`data[key]`) cannot be used to access fields in
structs, since structs do not implement the `Access` behaviour by
default. It is also a design decision: the dynamic access lookup
The Access syntax (`foo[bar]`) cannot be used to access fields in
structs, since structs do not implement the Access behaviour by
default. It is also design decision: the dynamic access lookup
is meant to be used for dynamic key-value structures, like maps
and keywords, and not by static ones like structs (where fields are
known and not dynamic).
and keywords, and not by static ones like structs.
Therefore Elixir provides a static lookup for struct fields and for atom
fields in maps. Imagine a struct named `User` with a `:name` field.
Therefore Elixir provides a static lookup for map and structs
fields. Imagine a struct named `User` with name and age fields.
The following would raise:
user = %User{name: "John"}
user = %User{name: "john"}
user[:name]
# ** (UndefinedFunctionError) undefined function User.fetch/2 (User does not implement the Access behaviour)
** (UndefinedFunctionError) undefined function User.fetch/2
(User does not implement the Access behaviour)
Structs instead use the `user.name` syntax to access fields:
Structs instead use the `user.name` syntax:
user.name
#=> "John"
#=> "john"
The same `user.name` syntax can also be used by `Kernel.put_in/2`
for updating structs fields:
to for updating structs fields:
put_in user.name, "Mary"
#=> %User{name: "Mary"}
put_in user.name, "mary"
%User{name: "mary"}
Differently from `user[:name]`, `user.name` is not extensible via
a behaviour and is restricted only to structs and atom keys in maps.
As mentioned above, this works for atom keys in maps as well. Refer to the
`Map` module for more information on this.
a behaviour and is restricted to only maps and structs.
Summing up:
@@ -106,132 +100,29 @@ defmodule Access do
functions for traversing other structures, like tuples and lists,
to be used alongside `Kernel.put_in/2` in others.
For instance, given a user map with `:name` and `:languages` keys, here is how
to deeply traverse the map and convert all language names to uppercase:
For instance, given a user with a list of languages, here is how to
deeply traverse the map and convert all language names to uppercase:
iex> languages = [
...> %{name: "elixir", type: :functional},
...> %{name: "c", type: :procedural},
...> ]
iex> user = %{name: "john", languages: languages}
iex> user = %{name: "john",
...> languages: [%{name: "elixir", type: :functional},
...> %{name: "c", type: :procedural}]}
iex> update_in user, [:languages, Access.all(), :name], &String.upcase/1
%{name: "john",
languages: [%{name: "ELIXIR", type: :functional},
%{name: "C", type: :procedural}]}
See the functions `key/1`, `key!/1`, `elem/1`, and `all/0` for some of the
available accessors.
## Implementing the Access behaviour for custom data structures
In order to be able to use the `Access` behaviour with custom data structures
(which have to be structs), such structures have to implement the `Access`
behaviour. For example, for a `User` struct, this would have to be done:
defmodule User do
defstruct [:name, :email]
@behaviour Access
# Implementation of the Access callbacks...
end
See the functions `key/1`, `key!/1`, `elem/1` and `all/0` for the current
accessors.
"""
@type container :: keyword | struct | map
@type nil_container :: nil
@type any_container :: any
@type t :: container | nil_container | any_container
@type t :: list | map | nil
@type key :: any
@type value :: any
@type get_fun(data, get_value) ::
(:get, data, (term -> term) ->
{get_value, new_data :: container})
@type get_and_update_fun(data, get_value) ::
(:get_and_update, data, (term -> term) ->
{get_value, new_data :: container} | :pop)
@type access_fun(data, get_value) ::
get_fun(data, get_value) | get_and_update_fun(data, get_value)
@doc """
Invoked in order to access the value stored under `key` in the given term `term`.
This function should return `{:ok, value}` where `value` is the value under
`key` if the key exists in the term, or `:error` if the key does not exist in
the term.
Many of the functions defined in the `Access` module internally call this
function. This function is also used when the square-brackets access syntax
(`structure[key]`) is used: the `fetch/2` callback implemented by the module
that defines the `structure` struct is invoked and if it returns `{:ok,
value}` then `value` is returned, or if it returns `:error` then `nil` is
returned.
See the `Map.fetch/2` and `Keyword.fetch/2` implementations for examples of
how to implement this callback.
"""
@callback fetch(term :: t, key) :: {:ok, value} | :error
@doc """
Invoked in order to access the value stored under `key` in the given term `term`,
defaulting to `default` if not present.
This function should return the value under `key` in `term` if there's
such key, otherwise `default`.
For most data structures, this can be implemented using `fetch/2` internally;
for example:
def get(structure, key, default) do
case fetch(structure, key) do
{:ok, value} -> value
:error -> default
end
end
See the `Map.get/3` and `Keyword.get/3` implementations for examples of
how to implement this callback.
"""
@callback get(term :: t, key, default :: value) :: value
@doc """
Invoked in order to access the value under `key` and update it at the same time.
The implementation of this callback should invoke `fun` with the value under
`key` in the passed structure `data`, or with `nil` if `key` is not present in it.
This function must return either `{get_value, update_value}` or `:pop`.
If the passed function returns `{get_value, update_value}`,
the return value of this callback should be `{get_value, new_data}`, where:
- `get_value` is the retrieved value (which can be operated on before being returned)
- `update_value` is the new value to be stored under `key`
- `new_data` is `data` after updating the value of `key` with `update_value`.
If the passed function returns `:pop`, the return value of this callback
must be `{value, new_data}` where `value` is the value under `key`
(or `nil` if not present) and `new_data` is `data` without `key`.
See the implementations of `Map.get_and_update/3` or `Keyword.get_and_update/3`
for more examples.
"""
@callback get_and_update(data, key, (value -> {get_value, value} | :pop)) ::
{get_value, data} when get_value: var, data: container | any_container
@doc """
Invoked to "pop" the value under `key` out of the given data structure.
When `key` exists in the given structure `data`, the implementation should
return a `{value, new_data}` tuple where `value` is the value that was under
`key` and `new_data` is `term` without `key`.
When `key` is not present in the given structure, a tuple `{value, data}`
should be returned, where `value` is implementation-defined.
See the implementations for `Map.pop/3` or `Keyword.pop/3` for more examples.
"""
@callback pop(data, key) :: {value, data} when data: container | any_container
@callback fetch(t, key) :: {:ok, value} | :error
@callback get(t, key, value) :: value
@callback get_and_update(t, key, (value -> {value, value} | :pop)) :: {value, t}
@callback pop(t, key) :: {value, t}
defmacrop raise_undefined_behaviour(e, struct, top) do
quote do
@@ -248,33 +139,25 @@ defmodule Access do
end
@doc """
Fetches the value for the given key in a container (a map, keyword
list, or struct that implements the `Access` behaviour).
Returns `{:ok, value}` where `value` is the value under `key` if there is such
a key, or `:error` if `key` is not found.
Fetches the container's value for the given key.
"""
@spec fetch(container, term) :: {:ok, term} | :error
@spec fetch(nil_container, any) :: :error
@spec fetch(t, term) :: {:ok, term} | :error
def fetch(container, key)
def fetch(%struct{} = container, key) do
def fetch(%{__struct__: struct} = container, key) do
struct.fetch(container, key)
rescue
e in UndefinedFunctionError ->
raise_undefined_behaviour e, struct, {^struct, :fetch, [^container, ^key], _}
end
def fetch(map, key) when is_map(map) do
case map do
%{^key => value} -> {:ok, value}
_ -> :error
end
def fetch(%{} = map, key) do
:maps.find(key, map)
end
def fetch(list, key) when is_list(list) and is_atom(key) do
case :lists.keyfind(key, 1, list) do
{_, value} -> {:ok, value}
{^key, value} -> {:ok, value}
false -> :error
end
end
@@ -289,67 +172,30 @@ defmodule Access do
end
@doc """
Gets the value for the given key in a container (a map, keyword
list, or struct that implements the `Access` behaviour).
Returns the value under `key` if there is such a key, or `default` if `key` is
not found.
Gets the container's value for the given key.
"""
@spec get(container, term, term) :: term
@spec get(nil_container, any, default) :: default when default: var
def get(container, key, default \\ nil)
def get(%{__struct__: struct} = container, key, default) do
try do
struct.fetch(container, key)
rescue
e in UndefinedFunctionError ->
raise_undefined_behaviour e, struct, {^struct, :fetch, [^container, ^key], _}
else
@spec get(t, term, term) :: term
def get(container, key, default \\ nil) do
case fetch(container, key) do
{:ok, value} -> value
:error -> default
end
end
def get(map, key, default) when is_map(map) do
case map do
%{^key => value} -> value
_ -> default
end
end
def get(list, key, default) when is_list(list) and is_atom(key) do
case :lists.keyfind(key, 1, list) do
{_, value} -> value
false -> default
end
end
def get(list, key, _default) when is_list(list) do
raise ArgumentError,
"the Access calls for keywords expect the key to be an atom, got: " <> inspect(key)
end
def get(nil, _key, default) do
default
end
@doc """
Gets and updates the given key in a `container` (a map, a keyword list,
a struct that implements the `Access` behaviour).
Gets and updates the container's value for the given key, in a single pass.
The `fun` argument receives the value of `key` (or `nil` if `key` is not
present in `container`) and must return a two-element tuple `{get_value, update_value}`:
the "get" value `get_value` (the retrieved value, which can be operated on before
being returned) and the new value to be stored under `key` (`update_value`).
`fun` may also return `:pop`, which means the current value
should be removed from the container and returned.
This `fun` argument receives the value of `key` (or `nil` if `key`
is not present) and must return a two-element tuple: the "get" value
(the retrieved value, which can be operated on before being returned)
and the new value to be stored under `key`. The `fun` may also
return `:pop`, implying the current value shall be removed
from the map and returned.
The returned value is a two-element tuple with the "get" value returned by
`fun` and a new container with the updated value under `key`.
The returned value is a tuple with the "get" value returned by
`fun` and a new map with the updated value under `key`.
"""
@spec get_and_update(data, key, (value -> {get_value, value} | :pop)) ::
{get_value, data} when get_value: var, data: container
@spec get_and_update(t, key, (value -> {get, value})) :: {get, t} when get: var
def get_and_update(container, key, fun)
def get_and_update(%{__struct__: struct} = container, key, fun) do
@@ -359,7 +205,7 @@ defmodule Access do
raise_undefined_behaviour e, struct, {^struct, :get_and_update, [^container, ^key, ^fun], _}
end
def get_and_update(map, key, fun) when is_map(map) do
def get_and_update(%{} = map, key, fun) do
Map.get_and_update(map, key, fun)
end
@@ -372,48 +218,19 @@ defmodule Access do
"could not put/update key #{inspect key} on a nil value"
end
@doc """
Removes the entry with a given key from a container (a map, keyword
list, or struct that implements the `Access` behaviour).
Returns a tuple containing the value associated with the key and the
updated container. `nil` is returned for the value if the key isn't
in the container.
## Examples
With a map:
iex> Access.pop(%{name: "Elixir", creator: "Valim"}, :name)
{"Elixir", %{creator: "Valim"}}
A keyword list:
iex> Access.pop([name: "Elixir", creator: "Valim"], :name)
{"Elixir", [creator: "Valim"]}
An unknown key:
iex> Access.pop(%{name: "Elixir", creator: "Valim"}, :year)
{nil, %{creator: "Valim", name: "Elixir"}}
"""
@spec pop(data, key) :: {value, data} when data: container
def pop(%{__struct__: struct} = container, key) do
struct.pop(container, key)
rescue
e in UndefinedFunctionError ->
raise_undefined_behaviour e, struct, {^struct, :pop, [^container, ^key], _}
end
def pop(list, key) when is_list(list), do: Keyword.pop(list, key)
def pop(map, key) when is_map(map) do
Map.pop(map, key)
case map do
%{^key => value} -> {value, :maps.remove(key, map)}
%{} -> {nil, map}
end
end
def pop(list, key) when is_list(list) do
Keyword.pop(list, key)
end
def pop(nil, key) do
raise ArgumentError,
"could not pop key #{inspect key} on a nil value"
@@ -422,27 +239,22 @@ defmodule Access do
## Accessors
@doc """
Returns a function that accesses the given key in a map/struct.
Accesses the given key in a map/struct.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
The returned function uses the default value if the key does not exist.
This can be used to specify defaults and safely traverse missing keys:
iex> get_in(%{}, [Access.key(:user, %{}), Access.key(:name)])
nil
Such is also useful when using update functions, allowing us to introduce
values as we traverse the data structure for updates:
iex> put_in(%{}, [Access.key(:user, %{}), Access.key(:name)], "Mary")
%{user: %{name: "Mary"}}
Uses the default value if the key does not exist
or if the value being accessed is `nil`.
## Examples
iex> get_in(%{}, [Access.key(:unknown), Access.key(:name)])
nil
iex> get_in(%{}, [Access.key(:unknown, %{name: "john"}), Access.key(:name)])
"john"
iex> get_in(%{}, [Access.key(:unknown), Access.key(:name, "john")])
"john"
iex> map = %{user: %{name: "john"}}
iex> get_in(map, [Access.key(:unknown, %{}), Access.key(:name, "john")])
iex> get_in(map, [Access.key(:unknown), Access.key(:name, "john")])
"john"
iex> get_and_update_in(map, [Access.key(:user), Access.key(:name)], fn
...> prev -> {prev, String.upcase(prev)}
@@ -451,22 +263,18 @@ defmodule Access do
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
{"john", %{user: %{}}}
An error is raised if the accessed structure is not a map or a struct:
iex> get_in(nil, [Access.key(:foo)])
** (BadMapError) expected a map, got: nil
An error is raised if the accessed structure is not a map/struct/nil:
iex> get_in([], [Access.key(:foo)])
** (BadMapError) expected a map, got: []
** (RuntimeError) Access.key/1 expected a map/struct or nil, got: []
"""
@spec key(key, term) :: access_fun(data :: struct | map, get_value :: term)
def key(key, default \\ nil) do
fn
:get, data, next ->
next.(Map.get(data, key, default))
next.(Map.get(to_map(data), key, default))
:get_and_update, data, next ->
value = Map.get(data, key, default)
value = Map.get(to_map(data), key, default)
case next.(value) do
{get, update} -> {get, Map.put(data, key, update)}
:pop -> {value, Map.delete(data, key)}
@@ -474,13 +282,14 @@ defmodule Access do
end
end
defp to_map(nil), do: %{}
defp to_map(%{} = map), do: map
defp to_map(data), do: raise "Access.key/1 expected a map/struct or nil, got: #{inspect data}"
@doc """
Returns a function that accesses the given key in a map/struct.
Accesses the given key in a map/struct.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
The returned function raises if the key does not exist.
Raises if the key does not exist.
## Examples
@@ -502,7 +311,6 @@ defmodule Access do
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
"""
@spec key!(key) :: access_fun(data :: struct | map, get_value :: term)
def key!(key) do
fn
:get, %{} = data, next ->
@@ -519,12 +327,9 @@ defmodule Access do
end
@doc ~S"""
Returns a function that accesses the element at the given index in a tuple.
Accesses the element at the given index in a tuple.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
The returned function raises if `index` is out of bounds.
Raises if the index is out of bounds.
## Examples
@@ -544,7 +349,6 @@ defmodule Access do
** (RuntimeError) Access.elem/1 expected a tuple, got: %{}
"""
@spec elem(non_neg_integer) :: access_fun(data :: tuple, get_value :: term)
def elem(index) when is_integer(index) do
pos = index + 1
@@ -563,10 +367,7 @@ defmodule Access do
end
@doc ~S"""
Returns a function that accesses all the elements in a list.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
Accesses all the elements in a list.
## Examples
@@ -581,7 +382,7 @@ defmodule Access do
{["john", "mary"], [%{}, %{}]}
Here is an example that traverses the list dropping even
numbers and multiplying odd numbers by 2:
numbers and multipling odd numbers by 2:
iex> require Integer
iex> get_and_update_in([1, 2, 3, 4, 5], [Access.all], fn
@@ -595,7 +396,6 @@ defmodule Access do
** (RuntimeError) Access.all/0 expected a list, got: %{}
"""
@spec all() :: access_fun(data :: list, get_value :: list)
def all() do
&all/3
end
@@ -605,7 +405,7 @@ defmodule Access do
end
defp all(:get_and_update, data, next) when is_list(data) do
all(data, next, _gets = [], _updates = [])
all(data, next, [], [])
end
defp all(_op, data, _next) do
@@ -624,10 +424,7 @@ defmodule Access do
end
@doc ~S"""
Returns a function that accesses the element at `index` (zero based) of a list.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
Accesses the element at `index` (zero based) of a list.
## Examples
@@ -667,10 +464,8 @@ defmodule Access do
iex> get_in(%{}, [Access.at(1)])
** (RuntimeError) Access.at/1 expected a list, got: %{}
"""
@spec at(non_neg_integer) :: access_fun(data :: list, get_value :: term)
def at(index) when is_integer(index) and index >= 0 do
def at(index) when index >= 0 do
fn(op, data, next) -> at(op, data, index, next) end
end
+61 -175
View File
@@ -6,18 +6,16 @@ defmodule Agent do
must be accessed from different processes or by the same process
at different points in time.
The `Agent` module provides a basic server implementation that
The Agent module provides a basic server implementation that
allows state to be retrieved and updated via a simple API.
## Examples
For example, in the Mix tool that ships with Elixir, we need
to keep a set of all tasks executed by a given project. Since
this set is shared, we can implement it with an agent:
this set is shared, we can implement it with an Agent:
defmodule Mix.TasksServer do
use Agent
def start_link do
Agent.start_link(fn -> MapSet.new end, name: __MODULE__)
end
@@ -44,11 +42,12 @@ defmodule Agent do
end
end
Agents provide a segregation between the client and server APIs (similar
to GenServers). In particular, the anonymous functions given to the `Agent`
are executed inside the agent (the server). This distinction is important
because you may want to avoid expensive operations inside the agent,
as they will effectively block the agent until the request is fulfilled.
Note that agents still provide a segregation between the
client and server APIs, as seen in GenServers. In particular,
all code inside the function passed to the agent is executed
by the agent. This distinction is important because you may
want to avoid expensive operations inside the agent, as it will
effectively block the agent until the request is fulfilled.
Consider these two examples:
@@ -62,36 +61,16 @@ defmodule Agent do
Agent.get(agent, &(&1)) |> do_something_expensive()
end
The first function blocks the agent. The second function copies all the state
to the client and then executes the operation in the client. One aspect to
consider is whether the data is large enough to require processing in the server,
at least initially, or small enough to be sent to the client cheaply. Another
factor is whether the data needs to be processed atomically: getting the
state and calling `do_something_expensive(state)` outside of the agent means
that the agent's state can be updated in the meantime. This is specially
important in case of updates as computing the new state in the client rather
than in the server can lead to race conditions if multiple clients are trying
to update the same state to different values.
The first function blocks the agent. The second function copies
all the state to the client and then executes the operation in the
client. The difference is whether the data is large enough to require
processing in the server, at least initially, or small enough to be
sent to the client cheaply.
Finally note `use Agent` defines a `child_spec/1` function, allowing the
defined module to be put under a supervision tree. The generated
`child_spec/1` can be customized with the following options:
## Name Registration
* `:id` - the child specification id, defauts to the current module
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
* `:restart` - when the child should be restarted, defaults to `:permanent`
* `:shutdown` - how to shut down the child
For example:
use Agent, restart: :transient, shutdown: 10_000
See the `Supervisor` docs for more information.
## Name registration
An agent is bound to the same name registration rules as GenServers.
Read more about it in the `GenServer` documentation.
An Agent is bound to the same name registration rules as GenServers.
Read more about it in the `GenServer` docs.
## A word on distributed agents
@@ -116,15 +95,14 @@ defmodule Agent do
## Hot code swapping
An agent can have its code hot swapped live by simply passing a module,
function, and arguments tuple to the update instruction. For example, imagine
function, and args tuple to the update instruction. For example, imagine
you have an agent named `:sample` and you want to convert its inner state
from a keyword list to a map. It can be done with the following
from some dict structure to a map. It can be done with the following
instruction:
{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}
The agent's state will be added to the given list of arguments (`[%{}]`) as
the first argument.
The agent's state will be added to the given list as the first argument.
"""
@typedoc "Return values of `start*` functions"
@@ -139,41 +117,13 @@ defmodule Agent do
@typedoc "The agent state"
@type state :: term
@doc false
def child_spec(arg) do
%{
id: Agent,
start: {Agent, :start_link, [arg]}
}
end
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
spec = [
id: opts[:id] || __MODULE__,
start: Macro.escape(opts[:start]) || quote(do: {__MODULE__, :start_link, [arg]}),
restart: opts[:restart] || :permanent,
shutdown: opts[:shutdown] || 5000,
type: :worker
]
@doc false
def child_spec(arg) do
%{unquote_splicing(spec)}
end
defoverridable child_spec: 1
end
end
@doc """
Starts an agent linked to the current process with the given function.
This is often used to start the agent as part of a supervision tree.
Once the agent is spawned, the given function `fun` is invoked and its return
value is used as the agent state. Note that `start_link/2` does not return
Once the agent is spawned, the given function is invoked and its return
value is used as the agent state. Note that `start_link` does not return
until the given function has returned.
## Options
@@ -194,22 +144,12 @@ defmodule Agent do
## Return values
If the server is successfully created and initialized, the function returns
`{:ok, pid}`, where `pid` is the PID of the server. If an agent with the
`{:ok, pid}`, where `pid` is the pid of the server. If an agent with the
specified name already exists, the function returns
`{:error, {:already_started, pid}}` with the PID of that process.
If the given function callback fails, the function returns `{:error, reason}`.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
iex> {:error, {exception, _stacktrace}} = Agent.start(fn -> raise "oops" end)
iex> exception
%RuntimeError{message: "oops"}
`{:error, {:already_started, pid}}` with the pid of that process.
If the given function callback fails with `reason`, the function returns
`{:error, reason}`.
"""
@spec start_link((() -> term), GenServer.options) :: on_start
def start_link(fun, options \\ []) when is_function(fun, 0) do
@@ -217,11 +157,11 @@ defmodule Agent do
end
@doc """
Starts an agent linked to the current process.
Starts an agent linked to the current process with the given module
function and arguments.
Same as `start_link/2` but a module, function, and arguments are expected
instead of an anonymous function; `fun` in `module` will be called with the
given arguments `args` to initialize the state.
Same as `start_link/2` but a module, function and args are expected
instead of an anonymous function.
"""
@spec start_link(module, atom, [any], GenServer.options) :: on_start
def start_link(module, fun, args, options \\ []) do
@@ -232,13 +172,6 @@ defmodule Agent do
Starts an agent process without links (outside of a supervision tree).
See `start_link/2` for more information.
## Examples
iex> {:ok, pid} = Agent.start(fn -> 42 end)
iex> Agent.get(pid, fn(state) -> state end)
42
"""
@spec start((() -> term), GenServer.options) :: on_start
def start(fun, options \\ []) when is_function(fun, 0) do
@@ -246,9 +179,10 @@ defmodule Agent do
end
@doc """
Starts an agent without links with the given module, function, and arguments.
Starts an agent with the given module function and arguments.
See `start_link/4` for more information.
Similar to `start/2` but a module, function and args are expected
instead of an anonymous function.
"""
@spec start(module, atom, [any], GenServer.options) :: on_start
def start(module, fun, args, options \\ []) do
@@ -256,24 +190,13 @@ defmodule Agent do
end
@doc """
Gets an agent value via the given anonymous function.
Gets an agent value via the given function.
The function `fun` is sent to the `agent` which invokes the function
passing the agent state. The result of the function invocation is
returned from this function.
`timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
exits.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
returned.
A timeout can also be specified (it has a default value of 5000).
"""
@spec get(agent, (state -> a), timeout) :: a when a: var
def get(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
@@ -283,9 +206,9 @@ defmodule Agent do
@doc """
Gets an agent value via the given function.
Same as `get/3` but a module, function, and arguments are expected
Same as `get/3` but a module, function and args are expected
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
argument to the given list of args.
"""
@spec get(agent, module, atom, [term], timeout) :: any
def get(agent, module, fun, args, timeout \\ 5000) do
@@ -293,28 +216,14 @@ defmodule Agent do
end
@doc """
Gets and updates the agent state in one operation via the given anonymous
function.
Gets and updates the agent state in one operation.
The function `fun` is sent to the `agent` which invokes the function
passing the agent state. The function must return a tuple with two
elements, the first being the value to return (that is, the "get" value)
and the second one being the new state of the agent.
`timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
exits.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get_and_update(pid, fn state -> {state, state + 1} end)
42
iex> Agent.get(pid, fn state -> state end)
43
elements, the first being the value to return (i.e. the `get` value)
and the second one is the new state.
A timeout can also be specified (it has a default value of 5000).
"""
@spec get_and_update(agent, (state -> {a, state}), timeout) :: a when a: var
def get_and_update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
@@ -322,11 +231,11 @@ defmodule Agent do
end
@doc """
Gets and updates the agent state in one operation via the given function.
Gets and updates the agent state in one operation.
Same as `get_and_update/3` but a module, function, and arguments are expected
Same as `get_and_update/3` but a module, function and args are expected
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
argument to the given list of args.
"""
@spec get_and_update(agent, module, atom, [term], timeout) :: any
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
@@ -334,28 +243,13 @@ defmodule Agent do
end
@doc """
Updates the agent state via the given anonymous function.
Updates the agent state.
The function `fun` is sent to the `agent` which invokes the function
passing the agent state. The return value of `fun` becomes the new
state of the agent.
passing the agent state. The function must return the new state.
A timeout can also be specified (it has a default value of 5000).
This function always returns `:ok`.
`timeout` is an integer greater than zero which specifies how many
milliseconds are allowed before the agent executes the function and returns
the result value, or the atom `:infinity` to wait indefinitely. If no result
is received within the specified time, the function call fails and the caller
exits.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.update(pid, fn state -> state + 1 end)
:ok
iex> Agent.get(pid, fn state -> state end)
43
"""
@spec update(agent, (state -> state), timeout) :: :ok
def update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
@@ -363,11 +257,11 @@ defmodule Agent do
end
@doc """
Updates the agent state via the given function.
Updates the agent state.
Same as `update/3` but a module, function, and arguments are expected
Same as `update/3` but a module, function and args are expected
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
argument to the given list of args.
"""
@spec update(agent, module, atom, [term], timeout) :: :ok
def update(agent, module, fun, args, timeout \\ 5000) do
@@ -375,14 +269,13 @@ defmodule Agent do
end
@doc """
Performs a cast (*fire and forget*) operation on the agent state.
Performs a cast (fire and forget) operation on the agent state.
The function `fun` is sent to the `agent` which invokes the function
passing the agent state. The return value of `fun` becomes the new
state of the agent.
passing the agent state. The function must return the new state.
Note that `cast` returns `:ok` immediately, regardless of whether `agent` (or
the node it should live on) exists.
Note that `cast` returns `:ok` immediately, regardless of whether the
destination node or agent exists.
"""
@spec cast(agent, (state -> state)) :: :ok
def cast(agent, fun) when is_function(fun, 1) do
@@ -390,11 +283,11 @@ defmodule Agent do
end
@doc """
Performs a cast (*fire and forget*) operation on the agent state.
Performs a cast (fire and forget) operation on the agent state.
Same as `cast/2` but a module, function, and arguments are expected
Same as `cast/2` but a module, function and args are expected
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
argument to the given list of args.
"""
@spec cast(agent, module, atom, [term]) :: :ok
def cast(agent, module, fun, args) do
@@ -402,25 +295,18 @@ defmodule Agent do
end
@doc """
Synchronously stops the agent with the given `reason`.
Stops the agent with the given `reason`.
It returns `:ok` if the agent terminates with the given
reason. If the agent terminates with another reason, the call will
It returns `:ok` if the server terminates with the given
reason, if it terminates with another reason, the call will
exit.
This function keeps OTP semantics regarding error reporting.
If the reason is any other than `:normal`, `:shutdown` or
`{:shutdown, _}`, an error report will be logged.
## Examples
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.stop(pid)
:ok
"""
@spec stop(agent, reason :: term, timeout) :: :ok
def stop(agent, reason \\ :normal, timeout \\ :infinity) do
GenServer.stop(agent, reason, timeout)
:gen.stop(agent, reason, timeout)
end
end
+24 -88
View File
@@ -9,64 +9,24 @@ defmodule Application do
Applications are defined with an application file named `APP.app` where
`APP` is the application name, usually in `underscore_case`. The application
file must reside in the same `ebin` directory as the compiled modules of the
application. In Elixir, the Mix build tool is responsible for compiling your
source code and generating your application `.app` file. You can learn more
about the generation of `.app` files by typing `mix help compile.app`.
application.
Once your application is compiled, running your system is a matter of starting
your current application and its dependencies. Differently from other languages,
Elixir does not have a `main` procedure that is responsible for starting your
system. Instead, you start one or more applications, each with their own
initialization and termination logic.
In Elixir, Mix is responsible for compiling your source code and
generating your application `.app` file. Furthermore, Mix is also
responsible for configuring, starting and stopping your application
and its dependencies. For this reason, this documentation will focus
on the remaining aspects of your application: the application environment
and the application callback module.
Starting an application is done via the "application module callback", which
is a module that defines the `start/2` function. The `start/2` function should
then start a supervisor, which is often called as the top-level supervisor, since
it sits at the root of a potentially long supervision tree. When the system is
shutting down, all applications shut down their top-level supervisor, which
terminates children in the opposite order they are started.
We have mentioned the Mix build tool is responsible for compiling applications,
but it is also capable of running applications. For example, `mix test`
automatically starts your application dependencies and your application itself
before your test runs. `mix run --no-halt` also boots your current project and
can be used to start a long running system. See `mix help run`.
Developers can also use tools like [Distillery](https://github.com/bitwalker/distillery)
that build **releases**. Releases are able to package all of your source code
as well as the Erlang VM into a single directory. Releases also give you explicit
control over how each application is started and in which order. They also provide
a more streamlined mechanism for starting and stopping systems, debugging, logging,
as well as system monitoring.
Finally, Elixir provides tools such as escripts and archives, which are
different mechanisms for packaging your application. Those are typically used
when tools must be shared between developers and not as deployment options.
See `mix help archive.build` and `mix help escript.build` for more detail.
Shutting down a live system cleanly can be done by calling `System.stop/1`.
It will shut down all applications in the opposite order they are started.
Each application will then shutdown its top-level supervisor, if one is
available, which then shuts down its children.
From Erlang/OTP 19.1, a SIGTERM from the operating system will automatically
translate to `System.stop/0`. Erlang/OTP 20 gives user more explicit control
over OS signals via the `:os.set_signal/2` function.
Applications also provide an "application environment", which is how
applications are configured. The application environment can either be set
statically, via a configuration file, or dynamically via `put_env/3` and
friends.
Over the next sections, we will cover the "application environment" and
the "application module callback" in more detail.
You can learn more about Mix generation of `.app` files by typing
`mix help compile.app`.
## Application environment
Once an application is started, OTP provides an application environment
that can be used to configure the application.
Assuming you are inside a Mix project, you can edit the `application/0`
Assuming you are inside a Mix project, you can edit the `application`
function in the `mix.exs` file to the following:
def application do
@@ -80,13 +40,9 @@ defmodule Application do
Application.get_env(:APP_NAME, :hello)
#=> :world
Applications and dependencies in Mix projects are typically configured
via the `config/config.exs` file. For example, someone using your
application can configure the `:hello` key as follows:
config :APP_NAME, hello: :brand_new_world
It is also possible to configure applications dynamically via `put_env/3`.
It is also possible to put and delete values from the application value,
including new values that are not defined in the environment file (although
this should be avoided).
Keep in mind that each application is responsible for its environment.
Do not use the functions in this module for directly accessing or modifying
@@ -122,8 +78,8 @@ defmodule Application do
The `type` argument passed to `start/2` is usually `:normal` unless in a
distributed setup where application takeovers and failovers are configured.
Distributed applications is beyond the scope of this documentation. For those
interested on the topic, please access the OTP documentation:
This particular aspect of applications is explained in more detail in the
OTP documentation:
* [`:application` module](http://www.erlang.org/doc/man/application.html)
* [Applications – OTP Design Principles](http://www.erlang.org/doc/design_principles/applications.html)
@@ -132,18 +88,12 @@ defmodule Application do
by `use Application`) which does any application cleanup. It receives the
application state and can return any value. Note that shutting down the
supervisor is automatically handled by the VM.
An application without a supervision tree doesn't define an application
module callback in the application definition in `mix.exs` file. Even though
there is no module with application callbacks such as `start/2` and
`stop/1`, the application can be started and stopped the same way as an
application with a supervision tree.
"""
@doc """
Called when an application is started.
This function is called when an application is started using
This function is called when an the application is started using
`Application.start/2` (and functions on top of that, such as
`Application.ensure_started/2`). This function should start the top-level
process of the application (which should be the top supervisor of the
@@ -154,7 +104,7 @@ defmodule Application do
* `:normal` - used if the startup is a normal startup or if the application
is distributed and is started on the current node because of a failover
from another node and the application specification key `:start_phases`
from another mode and the application specification key `:start_phases`
is `:undefined`.
* `{:takeover, node}` - used if the application is distributed and is
started on the current node because of a failover on the node `node`.
@@ -166,10 +116,10 @@ defmodule Application do
specification key (e.g., `mod: {MyApp, [:my_args]}`).
This function should either return `{:ok, pid}` or `{:ok, pid, state}` if
startup is successful. `pid` should be the PID of the top supervisor. `state`
startup is successful. `pid` should be the pid of the top supervisor. `state`
can be an arbitrary term, and if omitted will default to `[]`; if the
application is later stopped, `state` is passed to the `stop/1` callback (see
the documentation for the `c:stop/1` callback for more information).
the documentation for the `stop/2` callback for more information).
`use Application` provides no default implementation for the `start/2`
callback.
@@ -195,20 +145,6 @@ defmodule Application do
"""
@callback stop(state) :: term
@doc """
Start an application in synchronous phases.
This function is called after `start/2` finishes but before
`Application.start/2` returns. It will be called once for every start phase
defined in the application's (and any included applications') specification,
in the order they are listed in.
"""
@callback start_phase(phase :: term, start_type, phase_args :: term) ::
:ok |
{:error, reason :: term}
@optional_callbacks start_phase: 3
@doc false
defmacro __using__(_) do
quote location: :keep do
@@ -219,7 +155,7 @@ defmodule Application do
:ok
end
defoverridable Application
defoverridable [stop: 1]
end
end
@@ -333,7 +269,7 @@ defmodule Application do
## Options
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
* `:timeout` - the timeout for the change (defaults to 5000ms)
* `:persistent` - persists the given value on application load and reloads
If `put_env/4` is called before the application is loaded, the application
@@ -398,7 +334,7 @@ defmodule Application do
started before this application is. If not, `{:error, {:not_started, app}}` is
returned, where `app` is the name of the missing application.
In case you want to automatically load **and start** all of `app`'s dependencies,
In case you want to automatically load **and start** all of `app`'s dependencies,
see `ensure_all_started/2`.
The `type` argument specifies the type of the application:
@@ -483,7 +419,7 @@ defmodule Application do
#=> "bar-123"
For more information on code paths, check the `Code` module in
Elixir and also Erlang's [`:code` module](http://www.erlang.org/doc/man/code.html).
Elixir and also Erlang's `:code` module.
"""
@spec app_dir(app) :: String.t
def app_dir(app) when is_atom(app) do
@@ -530,7 +466,7 @@ defmodule Application do
try do
do_format_error(reason)
catch
# A user could create an error that looks like a built-in one
# A user could create an error that looks like a builtin one
# causing an error.
:error, _ ->
inspect(reason)
+1 -2
View File
@@ -37,8 +37,7 @@ defmodule Atom do
:erlang.atom_to_list(atom)
end
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by v1.5
@doc false
@spec to_char_list(atom) :: charlist
def to_char_list(atom), do: Atom.to_charlist(atom)
+316 -442
View File
@@ -3,7 +3,7 @@ defmodule Base do
@moduledoc """
This module provides data encoding and decoding functions
according to [RFC 4648](https://tools.ietf.org/html/rfc4648).
according to [RFC 4648](http://tools.ietf.org/html/rfc4648).
This document defines the commonly used base 16, base 32, and base
64 encoding schemes.
@@ -92,145 +92,58 @@ defmodule Base do
"""
b16_alphabet = '0123456789ABCDEF'
b64_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
b64url_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
b32_alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
b32hex_alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUV'
b16_alphabet = Enum.with_index '0123456789ABCDEF'
b64_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
b64url_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
b32_alphabet = Enum.with_index 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
b32hex_alphabet = Enum.with_index '0123456789ABCDEFGHIJKLMNOPQRSTUV'
defmacrop encode_pair(alphabet, case, value) do
quote do
case unquote(value) do
unquote(encode_pair_clauses(alphabet, case))
end
end
end
defp encode_pair_clauses(alphabet, case) when case in [:sensitive, :upper] do
shift = shift(alphabet)
alphabet
|> Enum.with_index()
|> encode_clauses(shift)
end
defp encode_pair_clauses(alphabet, :lower) do
shift = shift(alphabet)
alphabet
|> Stream.map(fn c -> (if c in ?A..?Z, do: c - ?A + ?a, else: c) end)
|> Enum.with_index()
|> encode_clauses(shift)
end
defp shift(alphabet) do
alphabet
|> length()
|> :math.log2()
|> round()
end
defp encode_clauses(alphabet, shift) do
for {encoding1, value1} <- alphabet, {encoding2, value2} <- alphabet do
encoding = bsl(encoding1, 8) + encoding2
value = bsl(value1, shift) + value2
[clause] = quote do: (unquote(value) -> unquote(encoding))
clause
end
end
defmacrop decode_char(alphabet, case, encoding) do
quote do
case unquote(encoding) do
unquote(decode_char_clauses(alphabet, case))
end
end
end
defp decode_char_clauses(alphabet, case) when case in [:sensitive, :upper] do
clauses =
alphabet
|> Enum.with_index()
|> decode_clauses()
clauses ++ bad_digit_clause()
end
defp decode_char_clauses(alphabet, :lower) do
{uppers, rest} =
alphabet
|> Stream.with_index()
|> Enum.split_with(fn {encoding, _} -> encoding in ?A..?Z end)
lowers =
Enum.map(uppers, fn {encoding, value} -> {encoding - ?A + ?a, value} end)
if length(uppers) > length(rest) do
decode_mixed_clauses(lowers, rest)
else
decode_mixed_clauses(rest, lowers)
end
end
defp decode_char_clauses(alphabet, :mixed) when length(alphabet) == 16 do
alphabet = Enum.with_index(alphabet)
lowers =
alphabet
|> Stream.filter(fn {encoding, _} -> encoding in ?A..?Z end)
|> Enum.map(fn {encoding, value} -> {encoding - ?A + ?a, value} end)
decode_mixed_clauses(alphabet, lowers)
end
defp decode_char_clauses(alphabet, :mixed) when length(alphabet) == 32 do
alphabet
|> Stream.with_index()
|> Enum.flat_map(fn {encoding, value} = pair ->
if encoding in ?A..?Z do
[pair, {encoding - ?A + ?a, value}]
else
[pair]
end
end)
|> decode_clauses()
end
defp decode_mixed_clauses(first, second) do
first_clauses = decode_clauses(first)
second_clauses = decode_clauses(second) ++ bad_digit_clause()
join_clause =
quote do
encoding ->
case encoding do
unquote(second_clauses)
end
end
first_clauses ++ join_clause
end
defp decode_clauses(alphabet) do
Enum.each [{:enc16, :dec16, b16_alphabet},
{:enc32, :dec32, b32_alphabet},
{:enc64, :dec64, b64_alphabet},
{:enc64url, :dec64url, b64url_alphabet},
{:enc32hex, :dec32hex, b32hex_alphabet}], fn({enc, dec, alphabet}) ->
for {encoding, value} <- alphabet do
[clause] = quote do: (unquote(encoding) -> unquote(value))
clause
defp unquote(enc)(unquote(value)), do: unquote(encoding)
defp unquote(dec)(unquote(encoding)), do: unquote(value)
end
defp unquote(dec)(c) do
raise ArgumentError, "non-alphabet digit found: #{inspect <<c>>, binaries: :as_strings} (byte #{c})"
end
end
defp bad_digit_clause() do
quote do
c ->
raise ArgumentError,
"non-alphabet digit found: #{inspect <<c>>, binaries: :as_strings} (byte #{c})"
end
end
@compile {:inline, from_upper: 1, from_lower: 1, from_mixed: 1,
to_lower: 1, to_upper: 1, enc16: 1, dec16: 1,
enc32: 1, dec32: 1, enc32hex: 1, dec32hex: 1,
enc64: 1, dec64: 1, enc64url: 1, dec64url: 1}
defp maybe_pad(body, "", _, _),
do: body
defp maybe_pad(body, tail, false, _),
do: body <> tail
defp maybe_pad(body, tail, _, group_size) do
case group_size - rem(byte_size(tail), group_size) do
^group_size -> body <> tail
6 -> body <> tail <> "======"
5 -> body <> tail <> "====="
4 -> body <> tail <> "===="
3 -> body <> tail <> "==="
2 -> body <> tail <> "=="
1 -> body <> tail <> "="
defp to_lower(char) when char in ?A..?Z,
do: char + (?a - ?A)
defp to_lower(char),
do: char
defp to_upper(char), do: char
defp from_upper(char), do: char
defp from_lower(char) when char in ?a..?z,
do: char - (?a - ?A)
defp from_lower(char) when not char in ?A..?Z,
do: char
defp from_lower(char),
do: raise(ArgumentError, "non-alphabet digit found: \"#{<<char>>}\" (byte #{char})")
defp from_mixed(char) when char in ?a..?z,
do: char - (?a - ?A)
defp from_mixed(char),
do: char
defp maybe_pad(subject, false, _, _),
do: subject
defp maybe_pad(subject, _, group_size, pad) do
case rem(byte_size(subject), group_size) do
0 -> subject
x -> subject <> String.duplicate(pad, group_size - x)
end
end
@@ -245,8 +158,8 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - uses upper case characters (default)
* `:lower` - uses lower case characters
* `:upper` - use upper case characters (default)
* `:lower` - use lower case characters
## Examples
@@ -257,7 +170,8 @@ defmodule Base do
"666f6f626172"
"""
@spec encode16(binary, keyword) :: binary
@spec encode16(binary) :: binary
@spec encode16(binary, Keyword.t) :: binary
def encode16(data, opts \\ []) when is_binary(data) do
case = Keyword.get(opts, :case, :upper)
do_encode16(case, data)
@@ -274,9 +188,9 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters
* `:upper` - only allow upper case characters (default)
* `:lower` - only allow lower case characters
* `:mixed` - allow mixed case characters
## Examples
@@ -290,7 +204,8 @@ defmodule Base do
{:ok, "foobar"}
"""
@spec decode16(binary, keyword) :: {:ok, binary} | :error
@spec decode16(binary) :: {:ok, binary} | :error
@spec decode16(binary, Keyword.t) :: {:ok, binary} | :error
def decode16(string, opts \\ []) do
{:ok, decode16!(string, opts)}
rescue
@@ -308,9 +223,9 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters
* `:upper` - only allow upper case characters (default)
* `:lower` - only allow lower case characters
* `:mixed` - allow mixed case characters
An `ArgumentError` exception is raised if the padding is incorrect or
a non-alphabet character is present in the string.
@@ -327,7 +242,8 @@ defmodule Base do
"foobar"
"""
@spec decode16!(binary, keyword) :: binary
@spec decode16!(binary) :: binary
@spec decode16!(binary, Keyword.t) :: binary
def decode16!(string, opts \\ [])
def decode16!(string, opts) when is_binary(string) and rem(byte_size(string), 2) == 0 do
@@ -357,7 +273,8 @@ defmodule Base do
"Zm9vYg"
"""
@spec encode64(binary, keyword) :: binary
@spec encode64(binary) :: binary
@spec encode64(binary, Keyword.t) :: binary
def encode64(data, opts \\ []) when is_binary(data) do
pad? = Keyword.get(opts, :padding, true)
do_encode64(data, pad?)
@@ -387,7 +304,8 @@ defmodule Base do
{:ok, "foob"}
"""
@spec decode64(binary, keyword) :: {:ok, binary} | :error
@spec decode64(binary) :: {:ok, binary} | :error
@spec decode64(binary, Keyword.t) :: {:ok, binary} | :error
def decode64(string, opts \\ []) when is_binary(string) do
{:ok, decode64!(string, opts)}
rescue
@@ -421,7 +339,8 @@ defmodule Base do
"foob"
"""
@spec decode64!(binary, keyword) :: binary
@spec decode64!(binary) :: binary
@spec decode64!(binary, Keyword.t) :: binary
def decode64!(string, opts \\ []) when is_binary(string) do
pad? = Keyword.get(opts, :padding, true)
string |> remove_ignored(opts[:ignore]) |> do_decode64(pad?)
@@ -443,7 +362,8 @@ defmodule Base do
"_3_-_A"
"""
@spec url_encode64(binary, keyword) :: binary
@spec url_encode64(binary) :: binary
@spec url_encode64(binary, Keyword.t) :: binary
def url_encode64(data, opts \\ []) when is_binary(data) do
pad? = Keyword.get(opts, :padding, true)
do_encode64url(data, pad?)
@@ -471,7 +391,8 @@ defmodule Base do
{:ok, <<255, 127, 254, 252>>}
"""
@spec url_decode64(binary, keyword) :: {:ok, binary} | :error
@spec url_decode64(binary) :: {:ok, binary} | :error
@spec url_decode64(binary, Keyword.t) :: {:ok, binary} | :error
def url_decode64(string, opts \\ []) when is_binary(string) do
{:ok, url_decode64!(string, opts)}
rescue
@@ -503,7 +424,8 @@ defmodule Base do
<<255, 127, 254, 252>>
"""
@spec url_decode64!(binary, keyword) :: binary
@spec url_decode64!(binary) :: binary
@spec url_decode64!(binary, Keyword.t) :: binary
def url_decode64!(string, opts \\ []) when is_binary(string) do
pad? = Keyword.get(opts, :padding, true)
string |> remove_ignored(opts[:ignore]) |> do_decode64url(pad?)
@@ -521,8 +443,8 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - uses upper case characters (default)
* `:lower` - uses lower case characters
* `:upper` - use upper case characters (default)
* `:lower` - use lower case characters
The values for `:padding` can be:
@@ -541,7 +463,8 @@ defmodule Base do
"MZXW6YTBOI"
"""
@spec encode32(binary, keyword) :: binary
@spec encode32(binary) :: binary
@spec encode32(binary, Keyword.t) :: binary
def encode32(data, opts \\ []) when is_binary(data) do
case = Keyword.get(opts, :case, :upper)
pad? = Keyword.get(opts, :padding, true)
@@ -560,14 +483,14 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters
* `:upper` - only allow upper case characters (default)
* `:lower` - only allow lower case characters
* `:mixed` - allow mixed case characters
The values for `:padding` can be:
* `true` - requires the input string to be padded to the nearest multiple of 8 (default)
* `false` - ignores padding from the input string
* `true` - require the input string to be padded to the nearest multiple of 8 (default)
* `false` - ignore padding from the input string
## Examples
@@ -584,7 +507,8 @@ defmodule Base do
{:ok, "foobar"}
"""
@spec decode32(binary, keyword) :: {:ok, binary} | :error
@spec decode32(binary) :: {:ok, binary} | :error
@spec decode32(binary, Keyword.t) :: {:ok, binary} | :error
def decode32(string, opts \\ []) do
{:ok, decode32!(string, opts)}
rescue
@@ -606,14 +530,14 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters
* `:upper` - only allow upper case characters (default)
* `:lower` - only allow lower case characters
* `:mixed` - allow mixed case characters
The values for `:padding` can be:
* `true` - requires the input string to be padded to the nearest multiple of 8 (default)
* `false` - ignores padding from the input string
* `true` - require the input string to be padded to the nearest multiple of 8 (default)
* `false` - ignore padding from the input string
## Examples
@@ -630,7 +554,8 @@ defmodule Base do
"foobar"
"""
@spec decode32!(binary, keyword) :: binary
@spec decode32!(binary) :: binary
@spec decode32!(binary, Keyword.t) :: binary
def decode32!(string, opts \\ []) when is_binary(string) do
case = Keyword.get(opts, :case, :upper)
pad? = Keyword.get(opts, :padding, true)
@@ -650,8 +575,8 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - uses upper case characters (default)
* `:lower` - uses lower case characters
* `:upper` - use upper case characters (default)
* `:lower` - use lower case characters
The values for `:padding` can be:
@@ -670,11 +595,12 @@ defmodule Base do
"CPNMUOJ1E8"
"""
@spec hex_encode32(binary, keyword) :: binary
@spec hex_encode32(binary) :: binary
@spec hex_encode32(binary, Keyword.t) :: binary
def hex_encode32(data, opts \\ []) when is_binary(data) do
case = Keyword.get(opts, :case, :upper)
pad? = Keyword.get(opts, :padding, true)
do_encode32hex(case, data, pad?)
do_hex_encode32(case, data, pad?)
end
@doc """
@@ -690,14 +616,14 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters
* `:upper` - only allow upper case characters (default)
* `:lower` - only allow lower case characters
* `:mixed` - allow mixed case characters
The values for `:padding` can be:
* `true` - requires the input string to be padded to the nearest multiple of 8 (default)
* `false` - ignores padding from the input string
* `true` - require the input string to be padded to the nearest multiple of 8 (default)
* `false` - ignore padding from the input string
## Examples
@@ -714,7 +640,8 @@ defmodule Base do
{:ok, "foobar"}
"""
@spec hex_decode32(binary, keyword) :: {:ok, binary} | :error
@spec hex_decode32(binary) :: {:ok, binary} | :error
@spec hex_decode32(binary, Keyword.t) :: {:ok, binary} | :error
def hex_decode32(string, opts \\ []) do
{:ok, hex_decode32!(string, opts)}
rescue
@@ -737,14 +664,14 @@ defmodule Base do
The values for `:case` can be:
* `:upper` - only allows upper case characters (default)
* `:lower` - only allows lower case characters
* `:mixed` - allows mixed case characters
* `:upper` - only allow upper case characters (default)
* `:lower` - only allow lower case characters
* `:mixed` - allow mixed case characters
The values for `:padding` can be:
* `true` - requires the input string to be padded to the nearest multiple of 8 (default)
* `false` - ignores padding from the input string
* `true` - require the input string to be padded to the nearest multiple of 8 (default)
* `false` - ignore padding from the input string
## Examples
@@ -761,310 +688,257 @@ defmodule Base do
"foobar"
"""
@spec hex_decode32!(binary, keyword) :: binary
@spec hex_decode32!(binary) :: binary
@spec hex_decode32!(binary, Keyword.t) :: binary
def hex_decode32!(string, opts \\ []) when is_binary(string) do
case = Keyword.get(opts, :case, :upper)
pad? = Keyword.get(opts, :padding, true)
do_decode32hex(case, string, pad?)
do_hex_decode32(case, string, pad?)
end
defp remove_ignored(string, nil), do: string
defp remove_ignored(string, :whitespace) do
for <<char::8 <- string>>, char not in '\s\t\r\n', into: <<>>, do: <<char::8>>
end
enc16 = [upper: :enc16_upper, lower: :enc16_lower]
for {case, fun} <- enc16 do
defp unquote(fun)(char) do
encode_pair(unquote(b16_alphabet), unquote(case), char)
end
for <<c::8 <- string>>, not c in '\s\t\r\n', into: <<>>, do: <<c::8>>
end
defp do_encode16(_, <<>>), do: <<>>
for {case, fun} <- enc16 do
defp do_encode16(unquote(case), data) do
split = 8 * div(byte_size(data), 8)
<<main::size(split)-binary, rest::binary>> = data
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16,
unquote(fun)(c5)::16, unquote(fun)(c6)::16,
unquote(fun)(c7)::16, unquote(fun)(c8)::16>>
end
case rest do
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16,
unquote(fun)(c5)::16, unquote(fun)(c6)::16,
unquote(fun)(c7)::16>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16,
unquote(fun)(c5)::16, unquote(fun)(c6)::16>>
<<c1::8, c2::8, c3::8, c4::8, c5::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16,
unquote(fun)(c5)::16>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16, unquote(fun)(c4)::16>>
<<c1::8, c2::8, c3::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16,
unquote(fun)(c3)::16>>
<<c1::8, c2::8>> ->
<<main::binary, unquote(fun)(c1)::16, unquote(fun)(c2)::16>>
<<c1::8>> ->
<<main::binary, unquote(fun)(c1)::16>>
<<>> ->
main
end
end
defp do_encode16(:upper, data) do
for <<c::4 <- data>>, into: <<>>, do: <<enc16(c)::8>>
end
dec16 = [upper: :dec16_upper, lower: :dec16_lower, mixed: :dec16_mixed]
for {case, fun} <- dec16 do
defp unquote(fun)(encoding) do
decode_char(unquote(b16_alphabet), unquote(case), encoding)
end
defp do_encode16(:lower, data) do
for <<c::4 <- data>>, into: <<>>, do: <<to_lower(enc16(c))::8>>
end
defp do_decode16(_, <<>>), do: <<>>
for {case, fun} <- dec16 do
defp do_decode16(unquote(case), string) do
split = 8 * div(byte_size(string), 8)
<<main::size(split)-binary, rest::binary>> = string
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<unquote(fun)(c1)::4, unquote(fun)(c2)::4,
unquote(fun)(c3)::4, unquote(fun)(c4)::4,
unquote(fun)(c5)::4, unquote(fun)(c6)::4,
unquote(fun)(c7)::4, unquote(fun)(c8)::4>>
end
case rest do
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> ->
<<main::bits, unquote(fun)(c1)::4, unquote(fun)(c2)::4,
unquote(fun)(c3)::4, unquote(fun)(c4)::4,
unquote(fun)(c5)::4, unquote(fun)(c6)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<main::bits, unquote(fun)(c1)::4, unquote(fun)(c2)::4,
unquote(fun)(c3)::4, unquote(fun)(c4)::4>>
<<c1::8, c2::8>> ->
<<main::bits, unquote(fun)(c1)::4, unquote(fun)(c2)::4>>
<<_::8>> ->
raise ArgumentError, "odd-length string"
<<>> ->
main
end
defp do_decode16(:upper, string) when rem(byte_size(string), 2) == 0 do
for <<c1::8, c2::8 <- string>>, into: <<>> do
<<dec16(c1)::4, dec16(c2)::4>>
end
end
defp do_decode16(:lower, string) when rem(byte_size(string), 2) == 0 do
for <<c1::8, c2::8 <- string>>, into: <<>> do
<<dec16(from_lower(c1))::4, dec16(from_lower(c2))::4>>
end
end
defp do_decode16(:mixed, string) when rem(byte_size(string), 2) == 0 do
for <<c1::8, c2::8 <- string>>, into: <<>> do
<<dec16(from_mixed(c1))::4, dec16(from_mixed(c2))::4>>
end
end
for {base, alphabet} <- ["64": b64_alphabet, "64url": b64url_alphabet] do
pair = :"enc#{base}_pair"
char = :"enc#{base}_char"
do_encode = :"do_encode#{base}"
defp unquote(pair)(value) do
encode_pair(unquote(alphabet), :sensitive, value)
end
defp unquote(char)(value) do
value
|> unquote(pair)()
|> band(0x00FF)
end
defp unquote(do_encode)(<<>>, _), do: <<>>
defp unquote(do_encode)(data, pad?) do
split = 6 * div(byte_size(data), 6)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c1::12, c2::12, c3::12, c4::12 <- main>>, into: <<>> do
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(pair)(c3)::16, unquote(pair)(c4)::16>>
end
tail = case rest do
<<c1::12, c2::12, c3::12, c::4>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16, unquote(pair)(c3)::16,
unquote(char)(bsl(c, 2))::8>>
<<c1::12, c2::12, c3::8>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(pair)(bsl(c3, 4))::16>>
<<c1::12, c2::12>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16>>
<<c1::12, c2::4>> ->
<<unquote(pair)(c1)::16, unquote(char)(bsl(c2, 2))::8>>
<<c1::8>> ->
<<unquote(pair)(bsl(c1, 4))::16>>
<<>> ->
<<>>
end
maybe_pad(main, tail, pad?, 4)
defp do_encode64(<<>>, _), do: <<>>
defp do_encode64(data, pad?) do
split = 3 * div(byte_size(data), 3)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c::6 <- main>>, into: <<>>, do: <<enc64(c)::8>>
tail = case rest do
<<c1::6, c2::6, c3::4>> ->
<<enc64(c1)::8, enc64(c2)::8, enc64(bsl(c3, 2))::8>>
<<c1::6, c2::2>> ->
<<enc64(c1)::8, enc64(bsl(c2, 4))::8>>
<<>> ->
<<>>
end
main <> maybe_pad(tail, pad?, 4, "=")
end
for {base, alphabet} <- ["64": b64_alphabet, "64url": b64url_alphabet] do
fun = :"dec#{base}"
do_decode = :"do_decode#{base}"
defp unquote(fun)(encoding) do
decode_char(unquote(alphabet), :sensitive, encoding)
end
defp unquote(do_decode)(<<>>, _), do: <<>>
defp unquote(do_decode)(string, pad?) do
segs = div(byte_size(string) + 7, 8) - 1
<<main::size(segs)-binary-unit(64), rest::binary>> = string
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<unquote(fun)(c1)::6, unquote(fun)(c2)::6, unquote(fun)(c3)::6,
unquote(fun)(c4)::6, unquote(fun)(c5)::6, unquote(fun)(c6)::6,
unquote(fun)(c7)::6, unquote(fun)(c8)::6>>
end
case rest do
<<c1::8, c2::8, ?=, ?=>> ->
<<main::bits, unquote(fun)(c1)::6, bsr(unquote(fun)(c2), 4)::2>>
<<c1::8, c2::8, c3::8, ?=>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
bsr(unquote(fun)(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, ?=, ?=>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
bsr(unquote(fun)(c6), 4)::2>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
unquote(fun)(c6)::6, bsr(unquote(fun)(c7), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
unquote(fun)(c6)::6, unquote(fun)(c7)::6, unquote(fun)(c8)::6>>
<<c1::8, c2::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::6, bsr(unquote(fun)(c2), 4)::2>>
<<c1::8, c2::8, c3::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
bsr(unquote(fun)(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
bsr(unquote(fun)(c6), 4)::2>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::6, unquote(fun)(c2)::6,
unquote(fun)(c3)::6, unquote(fun)(c4)::6, unquote(fun)(c5)::6,
unquote(fun)(c6)::6, bsr(unquote(fun)(c7), 2)::4>>
_ ->
raise ArgumentError, "incorrect padding"
end
defp do_decode64(<<>>, _), do: <<>>
defp do_decode64(string, false) do
maybe_pad(string, true, 4, "=") |> do_decode64(true)
end
defp do_decode64(string, _pad?) when rem(byte_size(string), 4) == 0 do
split = byte_size(string) - 4
<<main::size(split)-binary, rest::binary>> = string
main = for <<c::8 <- main>>, into: <<>>, do: <<dec64(c)::6>>
tail = case rest do
<<c1::8, c2::8, ?=, ?=>> ->
<<dec64(c1)::6, bsr(dec64(c2), 4)::2>>
<<c1::8, c2::8, c3::8, ?=>> ->
<<dec64(c1)::6, dec64(c2)::6, bsr(dec64(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<dec64(c1)::6, dec64(c2)::6, dec64(c3)::6, dec64(c4)::6>>
<<>> ->
<<>>
end
main <> tail
end
defp do_decode64(_, _) do
raise ArgumentError, "incorrect padding"
end
for {base, alphabet} <- ["32": b32_alphabet, "32hex": b32hex_alphabet],
case <- [:upper, :lower] do
pair = :"enc#{base}_#{case}_pair"
char = :"enc#{base}_#{case}_char"
do_encode = :"do_encode#{base}"
defp unquote(pair)(value) do
encode_pair(unquote(alphabet), unquote(case), value)
defp do_encode64url(<<>>, _), do: <<>>
defp do_encode64url(data, pad?) do
split = 3 * div(byte_size(data), 3)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c::6 <- main>>, into: <<>>, do: <<enc64url(c)::8>>
tail = case rest do
<<c1::6, c2::6, c3::4>> ->
<<enc64url(c1)::8, enc64url(c2)::8, enc64url(bsl(c3, 2))::8>>
<<c1::6, c2::2>> ->
<<enc64url(c1)::8, enc64url(bsl(c2, 4))::8>>
<<>> ->
<<>>
end
main <> maybe_pad(tail, pad?, 4, "=")
end
defp unquote(char)(value) do
value
|> unquote(pair)()
|> band(0x00FF)
defp do_decode64url(<<>>, _), do: <<>>
defp do_decode64url(string, false) do
maybe_pad(string, true, 4, "=") |> do_decode64url(true)
end
defp do_decode64url(string, _pad?) when rem(byte_size(string), 4) == 0 do
split = byte_size(string) - 4
<<main::size(split)-binary, rest::binary>> = string
main = for <<c::8 <- main>>, into: <<>>, do: <<dec64url(c)::6>>
tail = case rest do
<<c1::8, c2::8, ?=, ?=>> ->
<<dec64url(c1)::6, bsr(dec64url(c2), 4)::2>>
<<c1::8, c2::8, c3::8, ?=>> ->
<<dec64url(c1)::6, dec64url(c2)::6, bsr(dec64url(c3), 2)::4>>
<<c1::8, c2::8, c3::8, c4::8>> ->
<<dec64url(c1)::6, dec64url(c2)::6, dec64url(c3)::6, dec64url(c4)::6>>
<<>> ->
<<>>
end
main <> tail
end
defp do_decode64url(_, _) do
raise ArgumentError, "incorrect padding"
end
defp unquote(do_encode)(_, <<>>, _), do: <<>>
defp unquote(do_encode)(unquote(case), data, pad?) do
defp do_encode32(_, <<>>, _), do: <<>>
for {case, fun} <- [upper: :to_upper, lower: :to_lower] do
defp do_encode32(unquote(case), data, pad?) do
split = 5 * div(byte_size(data), 5)
<<main::size(split)-binary, rest::binary>> = data
main =
for <<c1::10, c2::10, c3::10, c4::10 <- main>>, into: <<>> do
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(pair)(c3)::16, unquote(pair)(c4)::16>>
end
main = for <<c::5 <- main>>, into: <<>>, do: <<unquote(fun)(enc32(c))::8>>
tail = case rest do
<<c1::10, c2::10, c3::10, c4::2>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(pair)(c3)::16, unquote(char)(bsl(c4, 3))::8>>
<<c1::10, c2::10, c3::4>> ->
<<unquote(pair)(c1)::16, unquote(pair)(c2)::16,
unquote(char)(bsl(c3, 1))::8>>
<<c1::10, c2::6>> ->
<<unquote(pair)(c1)::16, unquote(pair)(bsl(c2, 4))::16>>
<<c1::8>> ->
<<unquote(pair)(bsl(c1, 2))::16>>
<<c1::5, c2::5, c3::5, c4::5, c5::5, c6::5, c7::2>> ->
<<unquote(fun)(enc32(c1))::8, unquote(fun)(enc32(c2))::8,
unquote(fun)(enc32(c3))::8, unquote(fun)(enc32(c4))::8,
unquote(fun)(enc32(c5))::8, unquote(fun)(enc32(c6))::8,
unquote(fun)(enc32(bsl(c7, 3)))::8>>
<<c1::5, c2::5, c3::5, c4::5, c5::4>> ->
<<unquote(fun)(enc32(c1))::8, unquote(fun)(enc32(c2))::8,
unquote(fun)(enc32(c3))::8, unquote(fun)(enc32(c4))::8,
unquote(fun)(enc32(bsl(c5, 1)))::8>>
<<c1::5, c2::5, c3::5, c4::1>> ->
<<unquote(fun)(enc32(c1))::8, unquote(fun)(enc32(c2))::8,
unquote(fun)(enc32(c3))::8, unquote(fun)(enc32(bsl(c4, 4)))::8>>
<<c1::5, c2::3>> ->
<<unquote(fun)(enc32(c1))::8, unquote(fun)(enc32(bsl(c2, 2)))::8>>
<<>> ->
<<>>
end
maybe_pad(main, tail, pad?, 8)
main <> maybe_pad(tail, pad?, 8, "=")
end
end
for {base, alphabet} <- ["32": b32_alphabet, "32hex": b32hex_alphabet],
case <- [:upper, :lower, :mixed] do
fun = :"dec#{base}_#{case}"
do_decode = :"do_decode#{base}"
defp do_decode32(_, <<>>, _), do: <<>>
defp do_decode32(case, string, false),
do: do_decode32(case, maybe_pad(string, true, 8, "="), true)
defp unquote(fun)(encoding) do
decode_char(unquote(alphabet), unquote(case), encoding)
end
defp unquote(do_decode)(_, <<>>, _), do: <<>>
defp unquote(do_decode)(unquote(case), string, pad?) do
segs = div(byte_size(string) + 7, 8) - 1
<<main::size(segs)-binary-unit(64), rest::binary>> = string
main =
for <<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8 <- main>>, into: <<>> do
<<unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
unquote(fun)(c5)::5, unquote(fun)(c6)::5,
unquote(fun)(c7)::5, unquote(fun)(c8)::5>>
end
case rest do
for {case, fun} <- [upper: :from_upper, lower: :from_lower, mixed: :from_mixed] do
defp do_decode32(unquote(case), string, _pad?) when rem(byte_size(string), 8) == 0 do
split = byte_size(string) - 8
<<main::size(split)-binary, rest::binary>> = string
main = for <<c::8 <- main>>, into: <<>>, do: <<dec32(unquote(fun)(c))::5>>
tail = case rest do
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
<<main::bits, unquote(fun)(c1)::5, bsr(unquote(fun)(c2), 2)::3>>
<<dec32(unquote(fun)(c1))::5, bsr(dec32(unquote(fun)(c2)), 2)::3>>
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, bsr(unquote(fun)(c4), 4)::1>>
<<dec32(unquote(fun)(c1))::5, dec32(unquote(fun)(c2))::5,
dec32(unquote(fun)(c3))::5, bsr(dec32(unquote(fun)(c4)), 4)::1>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
bsr(unquote(fun)(c5), 1)::4>>
<<dec32(unquote(fun)(c1))::5, dec32(unquote(fun)(c2))::5,
dec32(unquote(fun)(c3))::5, dec32(unquote(fun)(c4))::5,
bsr(dec32(unquote(fun)(c5)), 1)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
unquote(fun)(c5)::5, unquote(fun)(c6)::5,
bsr(unquote(fun)(c7), 3)::2>>
<<dec32(unquote(fun)(c1))::5, dec32(unquote(fun)(c2))::5,
dec32(unquote(fun)(c3))::5, dec32(unquote(fun)(c4))::5,
dec32(unquote(fun)(c5))::5, dec32(unquote(fun)(c6))::5,
bsr(dec32(unquote(fun)(c7)), 3)::2>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
unquote(fun)(c5)::5, unquote(fun)(c6)::5,
unquote(fun)(c7)::5, unquote(fun)(c8)::5>>
<<c1::8, c2::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::5, bsr(unquote(fun)(c2), 2)::3>>
<<c1::8, c2::8, c3::8, c4::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, bsr(unquote(fun)(c4), 4)::1>>
<<c1::8, c2::8, c3::8, c4::8, c5::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
bsr(unquote(fun)(c5), 1)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8>> when not pad? ->
<<main::bits, unquote(fun)(c1)::5, unquote(fun)(c2)::5,
unquote(fun)(c3)::5, unquote(fun)(c4)::5,
unquote(fun)(c5)::5, unquote(fun)(c6)::5,
bsr(unquote(fun)(c7), 3)::2>>
_ ->
raise ArgumentError, "incorrect padding"
<<dec32(unquote(fun)(c1))::5, dec32(unquote(fun)(c2))::5,
dec32(unquote(fun)(c3))::5, dec32(unquote(fun)(c4))::5,
dec32(unquote(fun)(c5))::5, dec32(unquote(fun)(c6))::5,
dec32(unquote(fun)(c7))::5, dec32(unquote(fun)(c8))::5>>
<<>> ->
<<>>
end
main <> tail
end
end
defp do_decode32(_, _, _),
do: raise ArgumentError, "incorrect padding"
defp do_hex_encode32(_, <<>>, _), do: <<>>
for {case, fun} <- [upper: :to_upper, lower: :to_lower] do
defp do_hex_encode32(unquote(case), data, pad?) do
split = 5 * div(byte_size(data), 5)
<<main::size(split)-binary, rest::binary>> = data
main = for <<c::5 <- main>>, into: <<>>, do: <<unquote(fun)(enc32hex(c))::8>>
tail = case rest do
<<c1::5, c2::5, c3::5, c4::5, c5::5, c6::5, c7::2>> ->
<<unquote(fun)(enc32hex(c1))::8, unquote(fun)(enc32hex(c2))::8,
unquote(fun)(enc32hex(c3))::8, unquote(fun)(enc32hex(c4))::8,
unquote(fun)(enc32hex(c5))::8, unquote(fun)(enc32hex(c6))::8,
unquote(fun)(enc32hex(bsl(c7, 3)))::8>>
<<c1::5, c2::5, c3::5, c4::5, c5::4>> ->
<<unquote(fun)(enc32hex(c1))::8, unquote(fun)(enc32hex(c2))::8,
unquote(fun)(enc32hex(c3))::8, unquote(fun)(enc32hex(c4))::8,
unquote(fun)(enc32hex(bsl(c5, 1)))::8>>
<<c1::5, c2::5, c3::5, c4::1>> ->
<<unquote(fun)(enc32hex(c1))::8, unquote(fun)(enc32hex(c2))::8,
unquote(fun)(enc32hex(c3))::8, unquote(fun)(enc32hex(bsl(c4, 4)))::8>>
<<c1::5, c2::3>> ->
<<unquote(fun)(enc32hex(c1))::8, unquote(fun)(enc32hex(bsl(c2, 2)))::8>>
<<>> ->
<<>>
end
main <> maybe_pad(tail, pad?, 8, "=")
end
end
defp do_hex_decode32(_, <<>>, _), do: <<>>
defp do_hex_decode32(case, string, false),
do: do_hex_decode32(case, maybe_pad(string, true, 8, "="), true)
for {case, fun} <- [upper: :from_upper, lower: :from_lower, mixed: :from_mixed] do
defp do_hex_decode32(unquote(case), string, _pad?) when rem(byte_size(string), 8) == 0 do
split = byte_size(string) - 8
<<main::size(split)-binary, rest::binary>> = string
main = for <<c::8 <- main>>, into: <<>>, do: <<dec32hex(unquote(fun)(c))::5>>
tail = case rest do
<<c1::8, c2::8, ?=, ?=, ?=, ?=, ?=, ?=>> ->
<<dec32hex(unquote(fun)(c1))::5, bsr(dec32hex(unquote(fun)(c2)), 2)::3>>
<<c1::8, c2::8, c3::8, c4::8, ?=, ?=, ?=, ?=>> ->
<<dec32hex(unquote(fun)(c1))::5, dec32hex(unquote(fun)(c2))::5,
dec32hex(unquote(fun)(c3))::5, bsr(dec32hex(unquote(fun)(c4)), 4)::1>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, ?=, ?=, ?=>> ->
<<dec32hex(unquote(fun)(c1))::5, dec32hex(unquote(fun)(c2))::5,
dec32hex(unquote(fun)(c3))::5, dec32hex(unquote(fun)(c4))::5,
bsr(dec32hex(unquote(fun)(c5)), 1)::4>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, ?=>> ->
<<dec32hex(unquote(fun)(c1))::5, dec32hex(unquote(fun)(c2))::5,
dec32hex(unquote(fun)(c3))::5, dec32hex(unquote(fun)(c4))::5,
dec32hex(unquote(fun)(c5))::5, dec32hex(unquote(fun)(c6))::5,
bsr(dec32hex(unquote(fun)(c7)), 3)::2>>
<<c1::8, c2::8, c3::8, c4::8, c5::8, c6::8, c7::8, c8::8>> ->
<<dec32hex(unquote(fun)(c1))::5, dec32hex(unquote(fun)(c2))::5,
dec32hex(unquote(fun)(c3))::5, dec32hex(unquote(fun)(c4))::5,
dec32hex(unquote(fun)(c5))::5, dec32hex(unquote(fun)(c6))::5,
dec32hex(unquote(fun)(c7))::5, dec32hex(unquote(fun)(c8))::5>>
<<>> ->
<<>>
end
main <> tail
end
end
defp do_hex_decode32(_, _, _),
do: raise ArgumentError, "incorrect padding"
end
+5 -12
View File
@@ -2,14 +2,13 @@ defmodule Behaviour do
@moduledoc """
This module has been deprecated.
Instead of `defcallback/1` and `defmacrocallback/1`, the `@callback` and
`@macrocallback` module attributes can be used (respectively). See the
documentation for `Module` for more information on these attributes.
Instead of `MyModule.__behaviour__(:callbacks)`,
`MyModule.behaviour_info(:callbacks)` can be used.
Instead of `defcallback`, one can simply use `@callback`.
Instead of `defmacrocallback`, one can simply use `@macrocallback`.
Instead of `__behaviour__(:callbacks)`, one can simply use `behaviour_info(:callbacks)`.
"""
# TODO: Deprecate by 1.4
@doc """
Defines a function callback according to the given type specification.
"""
@@ -80,12 +79,6 @@ defmodule Behaviour do
@doc false
defmacro __using__(_) do
quote do
warning =
"the Behaviour module is deprecated. Instead of using this module, " <>
"use the @callback and @macrocallback module attributes. See the " <>
"documentation for Module for more information on these attributes"
IO.warn(warning)
@doc false
def __behaviour__(:callbacks) do
__MODULE__.behaviour_info(:callbacks)
+2 -2
View File
@@ -14,8 +14,8 @@ defmodule Bitwise do
If you prefer to use only operators or skip them, you can
pass the following options:
* `:only_operators` - includes only operators
* `:skip_operators` - skips operators
* `:only_operators` - include only operators
* `:skip_operators` - skip operators
For example:
+1187 -129
View File
File diff suppressed because it is too large Load Diff
-597
View File
@@ -1,597 +0,0 @@
defmodule Date do
@moduledoc """
A Date struct and functions.
The Date struct contains the fields year, month, day and calendar.
New dates can be built with the `new/3` function or using the `~D`
sigil:
iex> ~D[2000-01-01]
~D[2000-01-01]
Both `new/3` and sigil return a struct where the date fields can
be accessed directly:
iex> date = ~D[2000-01-01]
iex> date.year
2000
iex> date.month
1
The functions on this module work with the `Date` struct as well
as any struct that contains the same fields as the `Date` struct,
such as `NaiveDateTime` and `DateTime`. Such functions expect
`t:Calendar.date/0` in their typespecs (instead of `t:t/0`).
Developers should avoid creating the Date structs directly
and instead rely on the functions provided by this module as well
as the ones in 3rd party calendar libraries.
## Comparing dates
Comparisons in Elixir using `==`, `>`, `<` and similar are structural
and based on the `Date` struct fields. For proper comparison between
dates, use the `compare/2` function.
## Using epochs
The `add/2` and `diff/2` functions can be used for computing dates
or retrieving the amount of days betweens instants. For example, if there
is an interest in computing the amount of days from the Unix epoch
(1970-01-01):
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
14716
iex> Date.add(~D[1970-01-01], 14716)
~D[2010-04-17]
Those functions are optimized to deal with common epochs, such
as the Unix Epoch above or the Gregorian Epoch (0000-01-01).
"""
@enforce_keys [:year, :month, :day]
defstruct [:year, :month, :day, calendar: Calendar.ISO]
@type t :: %Date{year: Calendar.year, month: Calendar.month,
day: Calendar.day, calendar: Calendar.calendar}
@doc """
Returns a range of dates.
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.
## Examples
iex> Date.range(~D[1999-01-01], ~D[2000-01-01])
#DateRange<~D[1999-01-01], ~D[2000-01-01]>
iex> Date.range(~N[2000-01-01 09:00:00], ~D[1999-01-01])
#DateRange<~N[2000-01-01 09:00:00], ~D[1999-01-01]>
A range of dates implements the `Enumerable` protocol, which means
functions in the `Enum` module can be used to work with
ranges:
iex> range = Date.range(~D[2001-01-01], ~D[2002-01-01])
iex> Enum.count(range)
366
iex> Enum.member?(range, ~D[2001-02-01])
true
iex> Enum.reduce(range, 0, fn _date, acc -> acc - 1 end)
-366
"""
@spec range(Calendar.date, Calendar.date) :: Date.Range.t
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
{first_days, _} = to_iso_days(first)
{last_days, _} = to_iso_days(last)
%Date.Range{
first: first,
last: last,
first_in_iso_days: first_days,
last_in_iso_days: last_days,
}
end
def range(%{calendar: _, year: _, month: _, day: _},
%{calendar: _, year: _, month: _, day: _}) do
raise ArgumentError, "both dates must have matching calendars"
end
@doc """
Returns the current date in UTC.
## Examples
iex> date = Date.utc_today()
iex> date.year >= 2016
true
"""
@spec utc_today(Calendar.calendar) :: t
def utc_today(calendar \\ Calendar.ISO)
def utc_today(Calendar.ISO) do
{:ok, {year, month, day}, _, _} = Calendar.ISO.from_unix(System.os_time, :native)
%Date{year: year, month: month, day: day}
end
def utc_today(calendar) do
calendar
|> DateTime.utc_now
|> DateTime.to_date
end
@doc """
Returns true if the year in the given `date` is a leap year.
## Examples
iex> Date.leap_year?(~D[2000-01-01])
true
iex> Date.leap_year?(~D[2001-01-01])
false
iex> Date.leap_year?(~D[2004-01-01])
true
iex> Date.leap_year?(~D[1900-01-01])
false
iex> Date.leap_year?(~N[2004-01-01 01:23:45])
true
"""
@spec leap_year?(Calendar.date) :: boolean()
def leap_year?(date)
def leap_year?(%{calendar: calendar, year: year}) do
calendar.leap_year?(year)
end
@doc """
Returns the number of days in the given `date` month.
## Examples
iex> Date.days_in_month(~D[1900-01-13])
31
iex> Date.days_in_month(~D[1900-02-09])
28
iex> Date.days_in_month(~N[2000-02-20 01:23:45])
29
"""
@spec days_in_month(Calendar.date) :: Calendar.day
def days_in_month(date)
def days_in_month(%{calendar: calendar, year: year, month: month}) do
calendar.days_in_month(year, month)
end
@doc """
Builds a new ISO date.
Expects all values to be integers. Returns `{:ok, date}` if each
entry fits its appropriate range, returns `{:error, reason}` otherwise.
## Examples
iex> Date.new(2000, 1, 1)
{:ok, ~D[2000-01-01]}
iex> Date.new(2000, 13, 1)
{:error, :invalid_date}
iex> Date.new(2000, 2, 29)
{:ok, ~D[2000-02-29]}
iex> Date.new(2000, 2, 30)
{:error, :invalid_date}
iex> Date.new(2001, 2, 29)
{:error, :invalid_date}
"""
@spec new(Calendar.year, Calendar.month, Calendar.day) :: {:ok, t} | {:error, atom}
def new(year, month, day, calendar \\ Calendar.ISO) do
if calendar.valid_date?(year, month, day) do
{:ok, %Date{year: year, month: month, day: day, calendar: calendar}}
else
{:error, :invalid_date}
end
end
@doc """
Converts the given date to a string according to its calendar.
### Examples
iex> Date.to_string(~D[2000-02-28])
"2000-02-28"
iex> Date.to_string(~N[2000-02-28 01:23:45])
"2000-02-28"
"""
@spec to_string(Calendar.date) :: String.t
def to_string(date)
def to_string(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.date_to_string(year, month, day)
end
@doc """
Parses the extended "Dates" format described by
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
## Examples
iex> Date.from_iso8601("2015-01-23")
{:ok, ~D[2015-01-23]}
iex> Date.from_iso8601("2015:01:23")
{:error, :invalid_format}
iex> Date.from_iso8601("2015-01-32")
{:error, :invalid_date}
"""
@spec from_iso8601(String.t) :: {:ok, t} | {:error, atom}
def from_iso8601(string, calendar \\ Calendar.ISO)
def from_iso8601(<<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes>>, calendar) do
with {year, ""} <- Integer.parse(year),
{month, ""} <- Integer.parse(month),
{day, ""} <- Integer.parse(day) do
with {:ok, date} <- new(year, month, day, Calendar.ISO),
do: convert(date, calendar)
else
_ -> {:error, :invalid_format}
end
end
def from_iso8601(<<_::binary>>, _calendar) do
{:error, :invalid_format}
end
@doc """
Parses the extended "Dates" format described by
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
Raises if the format is invalid.
## Examples
iex> Date.from_iso8601!("2015-01-23")
~D[2015-01-23]
iex> Date.from_iso8601!("2015:01:23")
** (ArgumentError) cannot parse "2015:01:23" as date, reason: :invalid_format
"""
@spec from_iso8601!(String.t) :: t
def from_iso8601!(string, calendar \\ Calendar.ISO) do
case from_iso8601(string, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot parse #{inspect string} as date, reason: #{inspect reason}"
end
end
@doc """
Converts the given `date` to
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
By default, `Date.to_iso8601/2` returns dates formatted in the "extended"
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
Only supports converting dates which are in the ISO calendar,
or other calendars in which the days also start at midnight.
Attempting to convert dates from other calendars will raise an `ArgumentError`.
### Examples
iex> Date.to_iso8601(~D[2000-02-28])
"2000-02-28"
iex> Date.to_iso8601(~D[2000-02-28], :basic)
"20000228"
iex> Date.to_iso8601(~N[2000-02-28 00:00:00])
"2000-02-28"
"""
@spec to_iso8601(Calendar.date, :extended | :basic) :: String.t
def to_iso8601(date, format \\ :extended) when format in [:basic, :extended] do
%{year: year, month: month, day: day} = convert!(date, Calendar.ISO)
Calendar.ISO.date_to_iso8601(year, month, day, format)
end
@doc """
Converts the given `date` to an Erlang date tuple.
Only supports converting dates which are in the ISO calendar,
or other calendars in which the days also start at midnight.
Attempting to convert dates from other calendars will raise.
## Examples
iex> Date.to_erl(~D[2000-01-01])
{2000, 1, 1}
iex> Date.to_erl(~N[2000-01-01 00:00:00])
{2000, 1, 1}
"""
@spec to_erl(Calendar.date) :: :calendar.date
def to_erl(date) do
%{year: year, month: month, day: day} = convert!(date, Calendar.ISO)
{year, month, day}
end
@doc """
Converts an Erlang date tuple to a `Date` struct.
Only supports converting dates which are in the ISO calendar,
or other calendars in which the days also start at midnight.
Attempting to convert dates from other calendars will return an error tuple.
## Examples
iex> Date.from_erl({2000, 1, 1})
{:ok, ~D[2000-01-01]}
iex> Date.from_erl({2000, 13, 1})
{:error, :invalid_date}
"""
@spec from_erl(:calendar.date) :: {:ok, t} | {:error, atom}
def from_erl(tuple, calendar \\ Calendar.ISO)
def from_erl({year, month, day}, calendar) do
with {:ok, date} <- new(year, month, day, Calendar.ISO),
do: convert(date, calendar)
end
@doc """
Converts an Erlang date tuple but raises for invalid dates.
## Examples
iex> Date.from_erl!({2000, 1, 1})
~D[2000-01-01]
iex> Date.from_erl!({2000, 13, 1})
** (ArgumentError) cannot convert {2000, 13, 1} to date, reason: :invalid_date
"""
@spec from_erl!(:calendar.date) :: t
def from_erl!(tuple) do
case from_erl(tuple) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot convert #{inspect tuple} to date, reason: #{inspect reason}"
end
end
@doc """
Compares two date structs.
Returns `:gt` if first date is later than the second
and `:lt` for vice versa. If the two dates are equal
`:eq` is returned.
## Examples
iex> Date.compare(~D[2016-04-16], ~D[2016-04-28])
:lt
This function can also be used to compare across more
complex calendar types by considering only the date fields:
iex> Date.compare(~D[2016-04-16], ~N[2016-04-28 01:23:45])
:lt
iex> Date.compare(~D[2016-04-16], ~N[2016-04-16 01:23:45])
:eq
iex> Date.compare(~N[2016-04-16 12:34:56], ~N[2016-04-16 01:23:45])
:eq
"""
@spec compare(Calendar.date, Calendar.date) :: :lt | :eq | :gt
def compare(%{calendar: calendar, year: year1, month: month1, day: day1},
%{calendar: calendar, year: year2, month: month2, day: day2}) do
case {{year1, month1, day1}, {year2, month2, day2}} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
end
end
def compare(date1, date2) do
if Calendar.compatible_calendars?(date1.calendar, date2.calendar) do
case {to_iso_days(date1), to_iso_days(date2)} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
end
else
raise ArgumentError, """
cannot compare #{inspect date1} with #{inspect date2}.
This comparison would be ambiguous as their calendars have incompatible day rollover moments.
Specify an exact time of day (using `DateTime`s) to resolve this ambiguity
"""
end
end
@doc """
Converts the given `date` from it's calendar to the given `calendar`.
Returns `{:ok, date}` if the calendars are compatible,
or `{:error, :incompatible_calendars}` if they are not.
See also `Calendar.compatible_calendars?/2`.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
{:ok, %Date{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1}}
"""
@spec convert(Calendar.date, Calendar.calendar) :: {:ok, t} | {:error, :incompatible_calendars}
def convert(%{calendar: calendar, year: year, month: month, day: day}, calendar) do
{:ok, %Date{calendar: calendar, year: year, month: month, day: day}}
end
def convert(%{calendar: calendar} = date, target_calendar) do
if Calendar.compatible_calendars?(calendar, target_calendar) do
result_date =
date
|> to_iso_days()
|> from_iso_days(target_calendar)
{:ok, result_date}
else
{:error, :incompatible_calendars}
end
end
@doc """
Similar to `Date.convert/2`, but raises an `ArgumentError`
if the conversion between the two calendars is not possible.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
%Date{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1}
"""
@spec convert!(Calendar.date, Calendar.calendar) :: t
def convert!(date, calendar) do
case convert(date, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot convert #{inspect date} to target calendar #{inspect calendar}, reason: #{inspect reason}"
end
end
@doc """
Adds the number of days to the given `date`.
The days are counted as gregorian days. The date is returned in the same
calendar as it was given in.
## Examples
iex> Date.add(~D[2000-01-03], -2)
~D[2000-01-01]
iex> Date.add(~D[2000-01-01], 2)
~D[2000-01-03]
iex> Date.add(~N[2000-01-01 09:00:00], 2)
~D[2000-01-03]
"""
@spec add(Calendar.date, integer()) :: t
def add(%{calendar: calendar} = date, days) do
{iso_days_days, fraction} = to_iso_days(date)
from_iso_days({iso_days_days + days, fraction}, calendar)
end
@doc """
Calculates the difference between two dates, in a full number of days.
It returns the number of gregorian days between the dates. Only `Date`
structs that follow the same or compatible calendars can be compared
this way. If two calendars are not compatible, it will raise.
## Examples
iex> Date.diff(~D[2000-01-03], ~D[2000-01-01])
2
iex> Date.diff(~D[2000-01-01], ~D[2000-01-03])
-2
iex> Date.diff(~D[2000-01-01], ~N[2000-01-03 09:00:00])
-2
"""
@spec diff(Calendar.date, Calendar.date) :: integer
def diff(%{calendar: Calendar.ISO, year: year1, month: month1, day: day1},
%{calendar: Calendar.ISO, year: year2, month: month2, day: day2}) do
Calendar.ISO.date_to_iso_days_days(year1, month1, day1) -
Calendar.ISO.date_to_iso_days_days(year2, month2, day2)
end
def diff(%{calendar: calendar1} = date1, %{calendar: calendar2} = date2) do
if Calendar.compatible_calendars?(calendar1, calendar2) do
{days1, _} = to_iso_days(date1)
{days2, _} = to_iso_days(date2)
days1 - days2
else
raise ArgumentError, "cannot calculate the difference between #{inspect date1} and #{inspect date2} because their calendars are not compatible and thus the result would be ambiguous"
end
end
defp to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
{Calendar.ISO.date_to_iso_days_days(year, month, day), {0, 86400000000}}
end
defp to_iso_days(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.naive_datetime_to_iso_days(year, month, day, 0, 0, 0, {0, 0})
end
defp from_iso_days({days, _}, Calendar.ISO) do
{year, month, day} = Calendar.ISO.date_from_iso_days_days(days)
%Date{year: year, month: month, day: day, calendar: Calendar.ISO}
end
defp from_iso_days(iso_days, target_calendar) do
{year, month, day, _, _, _, _} = target_calendar.naive_datetime_from_iso_days(iso_days)
%Date{year: year, month: month, day: day, calendar: target_calendar}
end
@doc """
Calculates the day of the week of a given `date`.
Returns the day of the week as an integer. For the ISO 8601
calendar (the default), it is an integer from 1 to 7, where
1 is Monday and 7 is Sunday.
## Examples
iex> Date.day_of_week(~D[2016-10-31])
1
iex> Date.day_of_week(~D[2016-11-01])
2
iex> Date.day_of_week(~N[2016-11-01 01:23:45])
2
"""
@spec day_of_week(Calendar.date) :: non_neg_integer()
def day_of_week(date)
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.day_of_week(year, month, day)
end
## Helpers
defimpl String.Chars do
def to_string(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.date_to_string(year, month, day)
end
end
defimpl Inspect do
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do
"~D[" <> Calendar.ISO.date_to_string(year, month, day) <> "]"
end
def inspect(date, opts) do
Inspect.Any.inspect(date, opts)
end
end
end
-86
View File
@@ -1,86 +0,0 @@
defmodule Date.Range do
@moduledoc """
Returns an inclusive range between dates.
Ranges must be created with the `Date.range/2` function.
The following fields are public:
* `:first` - the initial date on the range
* `:last` - the last date on the range
The remaining fields are private and should not be accessed.
"""
@type t :: %__MODULE__{first: Date.t, last: Date.t,
first_in_iso_days: Calendar.days,
last_in_iso_days: Calendar.days}
defstruct [:first, :last, :first_in_iso_days, :last_in_iso_days]
defimpl Enumerable do
def member?(%{first: %{calendar: calendar, year: first_year, month: first_month, day: first_day},
last: %{calendar: calendar, year: last_year, month: last_month, day: last_day},
first_in_iso_days: first_in_iso_days, last_in_iso_days: last_in_iso_days},
%Date{calendar: calendar, year: year, month: month, day: day}) do
first = {first_year, first_month, first_day}
last = {last_year, last_month, last_day}
date = {year, month, day}
if first_in_iso_days <= last_in_iso_days do
{:ok, date >= first and date <= last}
else
{:ok, date >= last and date <= first}
end
end
def member?(_, _) do
{:ok, false}
end
def count(%Date.Range{first_in_iso_days: first_in_iso_days, last_in_iso_days: last_in_iso_days}) do
{:ok, abs(first_in_iso_days - last_in_iso_days) + 1}
end
def reduce(%Date.Range{first_in_iso_days: first_in_iso_days, last_in_iso_days: last_in_iso_days,
first: %{calendar: calendar}}, acc, fun) do
reduce(first_in_iso_days, last_in_iso_days, acc, fun, calendar, first_in_iso_days <= last_in_iso_days)
end
defp reduce(_x, _y, {:halt, acc}, _fun, _calendar, _up?) do
{:halted, acc}
end
defp reduce(x, y, {:suspend, acc}, fun, calendar, up?) do
{:suspended, acc, &reduce(x, y, &1, fun, calendar, up?)}
end
defp reduce(x, y, {:cont, acc}, fun, calendar, up? = true) when x <= y do
reduce(x + 1, y, fun.(date_from_iso_days_days(x, calendar), acc), fun, calendar, up?)
end
defp reduce(x, y, {:cont, acc}, fun, calendar, up? = false) when x >= y do
reduce(x - 1, y, fun.(date_from_iso_days_days(x, calendar), acc), fun, calendar, up?)
end
defp reduce(_, _, {:cont, acc}, _fun, _calendar, _up) do
{:done, acc}
end
defp date_from_iso_days_days(days, Calendar.ISO) do
{year, month, day} = Calendar.ISO.date_from_iso_days_days(days)
%Date{year: year, month: month, day: day, calendar: Calendar.ISO}
end
defp date_from_iso_days_days(days, calendar) do
{year, month, day, _, _, _, _} = calendar.naive_datetime_from_iso_days({days, {0, 86400000000}})
%Date{year: year, month: month, day: day, calendar: calendar}
end
end
defimpl Inspect do
def inspect(%Date.Range{first: first, last: last}, _) do
"#DateRange<" <> inspect(first) <> ", " <> inspect(last) <> ">"
end
end
end
-670
View File
@@ -1,670 +0,0 @@
defmodule DateTime do
@moduledoc """
A datetime implementation with a time zone.
This datetime can be seen as an ephemeral snapshot
of a datetime at a given time zone. For such purposes,
it also includes both UTC and Standard offsets, as
well as the zone abbreviation field used exclusively
for formatting purposes.
Remember, comparisons in Elixir using `==`, `>`, `<` and friends
are structural and based on the DateTime struct fields. For proper
comparison between datetimes, use the `compare/2` function.
The functions on this module work with the `DateTime` struct as well
as any struct that contains the same fields as the `DateTime` struct.
Such functions expect `t:Calendar.datetime/0` in their typespecs
(instead of `t:t/0`).
Developers should avoid creating the DateTime struct directly
and instead rely on the functions provided by this module as
well as the ones in 3rd party calendar libraries.
## Where are my functions?
You will notice this module only contains conversion
functions as well as functions that work on UTC. This
is because a proper DateTime implementation requires a
TimeZone database which currently is not provided as part
of Elixir.
Such may be addressed in upcoming versions, meanwhile,
use 3rd party packages to provide DateTime building and
similar functionality with time zone backing.
"""
@enforce_keys [:year, :month, :day, :hour, :minute, :second,
:time_zone, :zone_abbr, :utc_offset, :std_offset]
defstruct [:year, :month, :day, :hour, :minute, :second, :time_zone,
:zone_abbr, :utc_offset, :std_offset, microsecond: {0, 0}, calendar: Calendar.ISO]
@type t :: %__MODULE__{year: Calendar.year, month: Calendar.month, day: Calendar.day,
calendar: Calendar.calendar, hour: Calendar.hour, minute: Calendar.minute,
second: Calendar.second, microsecond: Calendar.microsecond,
time_zone: Calendar.time_zone, zone_abbr: Calendar.zone_abbr,
utc_offset: Calendar.utc_offset, std_offset: Calendar.std_offset}
@unix_days :calendar.date_to_gregorian_days({1970, 1, 1})
@doc """
Returns the current datetime in UTC.
## Examples
iex> datetime = DateTime.utc_now()
iex> datetime.time_zone
"Etc/UTC"
"""
@spec utc_now(Calendar.calendar) :: t
def utc_now(calendar \\ Calendar.ISO) do
System.os_time |> from_unix!(:native, calendar)
end
@doc """
Converts the given Unix time to `DateTime`.
The integer can be given in different unit
according to `System.convert_time_unit/3` and it will
be converted to microseconds internally.
Unix times are always in UTC and therefore the DateTime
will be returned in UTC.
## Examples
iex> {:ok, datetime} = DateTime.from_unix(1464096368)
iex> datetime
#DateTime<2016-05-24 13:26:08Z>
iex> {:ok, datetime} = DateTime.from_unix(1432560368868569, :microsecond)
iex> datetime
#DateTime<2015-05-25 13:26:08.868569Z>
The unit can also be an integer as in `t:System.time_unit/0`:
iex> {:ok, datetime} = DateTime.from_unix(143256036886856, 1024)
iex> datetime
#DateTime<6403-03-17 07:05:22.320Z>
Negative Unix times are supported, up to -62167219200 seconds,
which is equivalent to "0000-01-01T00:00:00Z" or 0 Gregorian seconds.
"""
@spec from_unix(integer, :native | System.time_unit, Calendar.calendar) :: {:ok, t} | {:error, atom}
def from_unix(integer, unit \\ :second, calendar \\ Calendar.ISO) when is_integer(integer) do
case Calendar.ISO.from_unix(integer, unit) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} ->
iso_datetime = %DateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}
convert(iso_datetime, calendar)
{:error, _} = error ->
error
end
end
@doc """
Converts the given Unix time to `DateTime`.
The integer can be given in different unit
according to `System.convert_time_unit/3` and it will
be converted to microseconds internally.
Unix times are always in UTC and therefore the DateTime
will be returned in UTC.
## Examples
# An easy way to get the Unix epoch is passing 0 to this function
iex> DateTime.from_unix!(0)
#DateTime<1970-01-01 00:00:00Z>
iex> DateTime.from_unix!(1464096368)
#DateTime<2016-05-24 13:26:08Z>
iex> DateTime.from_unix!(1432560368868569, :microsecond)
#DateTime<2015-05-25 13:26:08.868569Z>
"""
@spec from_unix!(integer, :native | System.time_unit, Calendar.calendar) :: t
def from_unix!(integer, unit \\ :second, calendar \\ Calendar.ISO) when is_atom(unit) do
case from_unix(integer, unit, calendar) do
{:ok, datetime} ->
datetime
{:error, :invalid_unix_time} ->
raise ArgumentError, "invalid Unix time #{integer}"
end
end
@doc """
Converts the given `NaiveDateTime` to `DateTime`.
It expects a time zone to put the NaiveDateTime in.
Currently it only supports "Etc/UTC" as time zone.
## Examples
iex> {:ok, datetime} = DateTime.from_naive(~N[2016-05-24 13:26:08.003], "Etc/UTC")
iex> datetime
#DateTime<2016-05-24 13:26:08.003Z>
"""
@spec from_naive(NaiveDateTime.t, Calendar.time_zone) :: {:ok, t}
def from_naive(naive_datetime, time_zone)
def from_naive(%NaiveDateTime{calendar: calendar,
hour: hour, minute: minute, second: second, microsecond: microsecond,
year: year, month: month, day: day}, "Etc/UTC") do
{:ok, %DateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
std_offset: 0, utc_offset: 0, zone_abbr: "UTC", time_zone: "Etc/UTC"}}
end
@doc """
Converts the given `NaiveDateTime` to `DateTime`.
It expects a time zone to put the NaiveDateTime in.
Currently it only supports "Etc/UTC" as time zone.
## Examples
iex> DateTime.from_naive!(~N[2016-05-24 13:26:08.003], "Etc/UTC")
#DateTime<2016-05-24 13:26:08.003Z>
"""
@spec from_naive!(NaiveDateTime.t, Calendar.time_zone) :: t
def from_naive!(naive_datetime, time_zone) do
case from_naive(naive_datetime, time_zone) do
{:ok, datetime} ->
datetime
{:error, reason} ->
raise ArgumentError, "cannot parse #{inspect naive_datetime} to datetime, reason: #{inspect reason}"
end
end
@doc """
Converts the given `datetime` to Unix time.
The `datetime` is expected to be using the ISO calendar
with a year greater than or equal to 0.
It will return the integer with the given unit,
according to `System.convert_time_unit/3`.
## Examples
iex> 1464096368 |> DateTime.from_unix!() |> DateTime.to_unix()
1464096368
iex> dt = %DateTime{calendar: Calendar.ISO, day: 20, hour: 18, microsecond: {273806, 6},
...> minute: 58, month: 11, second: 19, time_zone: "America/Montevideo",
...> utc_offset: -10800, std_offset: 3600, year: 2014, zone_abbr: "UYST"}
iex> DateTime.to_unix(dt)
1416517099
iex> flamel = %DateTime{calendar: Calendar.ISO, day: 22, hour: 8, microsecond: {527771, 6},
...> minute: 2, month: 3, second: 25, std_offset: 0, time_zone: "Etc/UTC",
...> utc_offset: 0, year: 1418, zone_abbr: "UTC"}
iex> DateTime.to_unix(flamel)
-17412508655
"""
@spec to_unix(Calendar.datetime, System.time_unit) :: integer
def to_unix(datetime, unit \\ :second)
def to_unix(%{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
{days, fraction} = to_iso_days(datetime)
unix_units = Calendar.ISO.iso_days_to_unit({days - @unix_days, fraction}, unit)
offset_units = System.convert_time_unit(utc_offset + std_offset, :second, unit)
unix_units - offset_units
end
@doc """
Converts the given `datetime` into a `NaiveDateTime`.
Because `NaiveDateTime` does not hold time zone information,
any time zone related data will be lost during the conversion.
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 1},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.to_naive(dt)
~N[2000-02-29 23:00:07.0]
"""
@spec to_naive(t) :: NaiveDateTime.t
def to_naive(%DateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
%NaiveDateTime{year: year, month: month, day: day, calendar: calendar,
hour: hour, minute: minute, second: second, microsecond: microsecond}
end
@doc """
Converts a `DateTime` into a `Date`.
Because `Date` does not hold time nor time zone information,
data will be lost during the conversion.
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.to_date(dt)
~D[2000-02-29]
"""
@spec to_date(t) :: Date.t
def to_date(%DateTime{year: year, month: month, day: day, calendar: calendar}) do
%Date{year: year, month: month, day: day, calendar: calendar}
end
@doc """
Converts a `DateTime` into `Time`.
Because `Time` does not hold date nor time zone information,
data will be lost during the conversion.
## Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 1},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.to_time(dt)
~T[23:00:07.0]
"""
@spec to_time(t) :: Time.t
def to_time(%DateTime{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}) do
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}
end
@doc """
Converts the given datetime to
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601) format.
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended"
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
Only supports converting datetimes which are in the ISO calendar,
attempting to convert datetimes from other calendars will raise.
WARNING: the ISO 8601 datetime format does not contain the time zone nor
its abbreviation, which means information is lost when converting to such
format.
### Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.to_iso8601(dt)
"2000-02-29T23:00:07+01:00"
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "UTC",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
iex> DateTime.to_iso8601(dt)
"2000-02-29T23:00:07Z"
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> DateTime.to_iso8601(dt, :extended)
"2000-02-29T23:00:07-04:00"
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> DateTime.to_iso8601(dt, :basic)
"20000229T230007-0400"
"""
@spec to_iso8601(Calendar.datetime, :extended | :basic ) :: String.t
def to_iso8601(datetime, format \\ :extended)
def to_iso8601(_, format) when format not in [:extended, :basic] do
raise ArgumentError, "DateTime.to_iso8601/2 expects format to be :extended or :basic, got: #{inspect format}"
end
def to_iso8601(%{calendar: Calendar.ISO, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}, format) do
Calendar.ISO.datetime_to_iso8601(year, month, day, hour, minute, second, microsecond,
time_zone, zone_abbr, utc_offset, std_offset, format)
end
def to_iso8601(%{calendar: _, year: _, month: _, day: _,
hour: _, minute: _, second: _, microsecond: _,
time_zone: _, zone_abbr: _, utc_offset: _, std_offset: _} = datetime, format) do
datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format)
end
@doc """
Parses the extended "Date and time of day" format described by
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
Since ISO8601 does not include the proper time zone, the given
string will be converted to UTC and its offset in seconds will be
returned as part of this function. Therefore offset information
must be present in the string.
As specified in the standard, the separator "T" may be omitted if
desired as there is no ambiguity within this function.
Time representations with reduced accuracy are not supported.
Note that while ISO8601 allows datetimes to specify 24:00:00 as the
zero hour of the next day, this notation is not supported by Elixir.
## Examples
iex> {:ok, datetime, 0} = DateTime.from_iso8601("2015-01-23T23:50:07Z")
iex> datetime
#DateTime<2015-01-23 23:50:07Z>
iex> {:ok, datetime, 9000} = DateTime.from_iso8601("2015-01-23T23:50:07.123+02:30")
iex> datetime
#DateTime<2015-01-23 21:20:07.123Z>
iex> {:ok, datetime, 9000} = DateTime.from_iso8601("2015-01-23T23:50:07,123+02:30")
iex> datetime
#DateTime<2015-01-23 21:20:07.123Z>
iex> DateTime.from_iso8601("2015-01-23P23:50:07")
{:error, :invalid_format}
iex> DateTime.from_iso8601("2015-01-23 23:50:07A")
{:error, :invalid_format}
iex> DateTime.from_iso8601("2015-01-23T23:50:07")
{:error, :missing_offset}
iex> DateTime.from_iso8601("2015-01-23 23:50:61")
{:error, :invalid_time}
iex> DateTime.from_iso8601("2015-01-32 23:50:07")
{:error, :invalid_date}
iex> DateTime.from_iso8601("2015-01-23T23:50:07.123-00:00")
{:error, :invalid_format}
iex> DateTime.from_iso8601("2015-01-23T23:50:07.123-00:60")
{:error, :invalid_format}
"""
@spec from_iso8601(String.t, Calendar.calendar) :: {:ok, t, Calendar.utc_offset} | {:error, atom}
def from_iso8601(string, calendar \\ Calendar.ISO)
def from_iso8601(<<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes, sep,
hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes, rest::binary>>, calendar) when sep in [?\s, ?T] do
with {year, ""} <- Integer.parse(year),
{month, ""} <- Integer.parse(month),
{day, ""} <- Integer.parse(day),
{hour, ""} <- Integer.parse(hour),
{minute, ""} <- Integer.parse(min),
{second, ""} <- Integer.parse(sec),
{microsecond, rest} <- Calendar.ISO.parse_microsecond(rest),
{:ok, date} <- Date.new(year, month, day),
{:ok, time} <- Time.new(hour, minute, second, microsecond),
{:ok, offset} <- parse_offset(rest) do
%{year: year, month: month, day: day} = date
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
{_, precision} = microsecond
datetime =
Calendar.ISO.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> apply_tz_offset(offset)
|> from_iso_days("Etc/UTC", "UTC", 0, 0, calendar, precision)
{:ok, %{datetime | microsecond: microsecond}, offset}
else
{:error, reason} -> {:error, reason}
_ -> {:error, :invalid_format}
end
end
def from_iso8601(_, _) do
{:error, :invalid_format}
end
defp parse_offset(rest) do
case Calendar.ISO.parse_offset(rest) do
{offset, ""} when is_integer(offset) -> {:ok, offset}
{nil, ""} -> {:error, :missing_offset}
_ -> {:error, :invalid_format}
end
end
@doc """
Converts the given `datetime` to a string according to its calendar.
### Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.to_string(dt)
"2000-02-29 23:00:07+01:00 CET Europe/Warsaw"
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "UTC",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
iex> DateTime.to_string(dt)
"2000-02-29 23:00:07Z"
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> DateTime.to_string(dt)
"2000-02-29 23:00:07-04:00 AMT America/Manaus"
"""
@spec to_string(Calendar.datetime) :: String.t
def to_string(datetime)
def to_string(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}) do
calendar.datetime_to_string(year, month, day, hour, minute, second, microsecond,
time_zone, zone_abbr, utc_offset, std_offset)
end
defimpl String.Chars do
def to_string(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}) do
calendar.datetime_to_string(year, month, day, hour, minute, second, microsecond,
time_zone, zone_abbr, utc_offset, std_offset)
end
end
defimpl Inspect do
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}, _) do
"#DateTime<" <> Calendar.ISO.datetime_to_string(year, month, day, hour, minute, second, microsecond,
time_zone, zone_abbr, utc_offset, std_offset) <> ">"
end
def inspect(datetime, opts) do
Inspect.Any.inspect(datetime, opts)
end
end
@doc """
Compares two datetime structs.
Returns `:gt` if first datetime is later than the second
and `:lt` for vice versa. If the two datetimes are equal
`:eq` is returned.
Note that both utc and stc offsets will be taken into
account when comparison is done.
## Examples
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> dt2 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.compare(dt1, dt2)
:gt
"""
@spec compare(Calendar.datetime, Calendar.datetime) :: :lt | :eq | :gt
def compare(%DateTime{utc_offset: utc_offset1, std_offset: std_offset1} = datetime1,
%DateTime{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2) do
{days1, {parts1, ppd1}} =
datetime1
|> to_iso_days()
|> apply_tz_offset(utc_offset1 + std_offset1)
{days2, {parts2, ppd2}} =
datetime2
|> to_iso_days()
|> apply_tz_offset(utc_offset2 + std_offset2)
# Ensure fraction tuples have same denominator.
iso_days1 = {days1, parts1 * ppd2}
iso_days2 = {days2, parts2 * ppd1}
case {iso_days1, iso_days2} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
end
end
@doc """
Subtracts `datetime2` from `datetime1`.
The answer can be returned in any `unit` available from `t:System.time_unit/0`.
This function returns the difference in seconds where seconds are measured
according to `Calendar.ISO`.
## Examples
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> dt2 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> DateTime.diff(dt1, dt2)
18000
iex> DateTime.diff(dt2, dt1)
-18000
"""
@spec diff(Calendar.datetime, Calendar.datetime) :: integer()
def diff(%{utc_offset: utc_offset1, std_offset: std_offset1} = datetime1,
%{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2, unit \\ :second) do
naive_diff =
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)) -
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit))
offset_diff =
(utc_offset2 + std_offset2) - (utc_offset1 + std_offset1)
naive_diff + System.convert_time_unit(offset_diff, :second, unit)
end
@doc """
Converts a given `datetime` from one calendar to another.
If it is not possible to convert unambiguously between the calendars
(see `Calendar.compatible_calendars?/2`), an `{:error, :incompatible_calendars}` tuple
is returned.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> DateTime.convert(dt1, Calendar.Holocene)
{:ok, %DateTime{calendar: Calendar.Holocene, day: 29, hour: 23,
microsecond: {0, 0}, minute: 0, month: 2, second: 7, std_offset: 0,
time_zone: "America/Manaus", utc_offset: -14400, year: 12000,
zone_abbr: "AMT"}}
"""
@spec convert(Calendar.datetime, Calendar.calendar) :: {:ok, t} | {:error, :incompatible_calendars}
def convert(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}, calendar) do
{:ok, %DateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}}
end
def convert(%{calendar: dt_calendar, microsecond: {_, precision}} = datetime, calendar) do
if Calendar.compatible_calendars?(dt_calendar, calendar) do
result_datetime =
datetime
|> to_iso_days
|> from_iso_days(datetime, calendar, precision)
{:ok, result_datetime}
else
{:error, :incompatible_calendars}
end
end
@doc """
Converts a given `datetime` from one calendar to another.
If it is not possible to convert unambiguously between the calendars
(see `Calendar.compatible_calendars?/2`), an ArgumentError is raised.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
iex> DateTime.convert!(dt1, Calendar.Holocene)
%DateTime{calendar: Calendar.Holocene, day: 29, hour: 23,
microsecond: {0, 0}, minute: 0, month: 2, second: 7, std_offset: 0,
time_zone: "America/Manaus", utc_offset: -14400, year: 12000,
zone_abbr: "AMT"}
"""
@spec convert!(Calendar.datetime, Calendar.calendar) :: t | no_return
def convert!(datetime, calendar) do
case convert(datetime, calendar) do
{:ok, value} ->
value
{:error, :incompatible_calendars} ->
raise ArgumentError, "cannot convert #{inspect datetime} to target calendar #{inspect calendar}, reason: #{inspect datetime.calendar} and #{inspect calendar} have different day rollover moments, making this conversion ambiguous"
end
end
defp to_iso_days(%{calendar: calendar,year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
end
defp from_iso_days(iso_days, datetime, calendar, precision) do
%{time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset} = datetime
from_iso_days(iso_days, time_zone, zone_abbr, utc_offset, std_offset, calendar, precision)
end
defp from_iso_days(iso_days, time_zone, zone_abbr, utc_offset, std_offset, calendar, precision) do
{year, month, day, hour, minute, second, {microsecond, _}} = calendar.naive_datetime_from_iso_days(iso_days)
%DateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: {microsecond, precision},
time_zone: time_zone, zone_abbr: zone_abbr, utc_offset: utc_offset, std_offset: std_offset}
end
defp apply_tz_offset(iso_days, offset) do
Calendar.ISO.add_day_fraction_to_iso_days(iso_days, -offset, 86400)
end
end
+60 -319
View File
@@ -7,165 +7,36 @@ defmodule Calendar.ISO do
today. The proleptic means the Gregorian rules for leap years are
applied for all time, consequently the dates give different results
before the year 1583 from when the Gregorian calendar was adopted.
Note that while ISO8601 allows times and datetimes to specify
24:00:00 as the zero hour of the next day, this notation is not
supported by Elixir.
"""
@behaviour Calendar
@unix_epoch 62167219200
@unix_start 1_000_000 * -@unix_epoch
@unix_end 1_000_000 * (315569519999 - @unix_epoch)
@unix_range_microseconds @unix_start..@unix_end
@type year :: 0..9999
@type month :: 1..12
@type day :: 1..31
@seconds_per_minute 60
@seconds_per_hour 60 * 60
@seconds_per_day 24 * 60 * 60 # Note that this does _not_ handle leap seconds.
@microseconds_per_second 1_000_000
@doc """
Returns the `t:Calendar.iso_days` format of the specified date.
Builds and validates an ISO date.
## Examples
iex> Calendar.ISO.naive_datetime_to_iso_days(0, 1, 1, 0, 0, 0, {0, 6})
{0, {0, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 12, 0, 0, {0, 6})
{730485, {43200000000, 86400000000}}
iex> Calendar.ISO.naive_datetime_to_iso_days(2000, 1, 1, 13, 0, 0, {0, 6})
{730485, {46800000000, 86400000000}}
iex> Calendar.ISO.date(2000, 1, 1)
{:ok, ~D[2000-01-01]}
iex> Calendar.ISO.date(2000, 13, 1)
{:error, :invalid_date}
iex> Calendar.ISO.date(2000, 2, 29)
{:ok, ~D[2000-02-29]}
iex> Calendar.ISO.date(2000, 2, 30)
{:error, :invalid_date}
iex> Calendar.ISO.date(2001, 2, 29)
{:error, :invalid_date}
"""
@spec naive_datetime_to_iso_days(Calendar.year, Calendar.month, Calendar.day,
Calendar.hour, Calendar.minute, Calendar.second,
Calendar.microsecond) :: Calendar.iso_days
def naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) do
{date_to_iso_days_days(year, month, day),
time_to_day_fraction(hour, minute, second, microsecond)}
def date(year, month, day) when is_integer(year) and is_integer(month) and is_integer(day) do
if :calendar.valid_date(year, month, day) and year <= 9999 do
{:ok, %Date{year: year, month: month, day: day}}
else
{:error, :invalid_date}
end
end
@doc """
Converts the `t:Calendar.iso_days` format to the datetime format specified by this calendar.
## Examples
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
{0, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730485, {0, 86400}})
{2000, 1, 1, 0, 0, 0, {0, 6}}
iex> Calendar.ISO.naive_datetime_from_iso_days({730485, {43200, 86400}})
{2000, 1, 1, 12, 0, 0, {0, 6}}
"""
@spec naive_datetime_from_iso_days(Calendar.iso_days) ::
{Calendar.year, Calendar.month, Calendar.day,
Calendar.hour, Calendar.minute, Calendar.second, Calendar.microsecond}
def naive_datetime_from_iso_days({days, day_fraction}) do
{year, month, day} = date_from_iso_days_days(days)
{hour, minute, second, microsecond} = time_from_day_fraction(day_fraction)
{year, month, day, hour, minute, second, microsecond}
end
@doc """
Returns the normalized day fraction of the specified time.
## Examples
iex> Calendar.ISO.time_to_day_fraction(0, 0, 0, {0, 6})
{0, 86400000000}
iex> Calendar.ISO.time_to_day_fraction(12, 34, 56, {123, 6})
{45296000123, 86400000000}
"""
@spec time_to_day_fraction(Calendar.hour, Calendar.minute,
Calendar.second, Calendar.microsecond) :: Calendar.day_fraction
def time_to_day_fraction(0, 0, 0, {0, _}) do
{0, 86400000000}
end
def time_to_day_fraction(hour, minute, second, {microsecond, _}) do
combined_seconds = hour * @seconds_per_hour + minute * @seconds_per_minute + second
{combined_seconds * @microseconds_per_second + microsecond, @seconds_per_day * @microseconds_per_second}
end
@doc """
Converts a day fraction to this Calendar's representation of time.
## Examples
iex> Calendar.ISO.time_from_day_fraction({1,2})
{12, 0, 0, {0, 6}}
iex> Calendar.ISO.time_from_day_fraction({13,24})
{13, 0, 0, {0, 6}}
"""
@spec time_from_day_fraction(Calendar.day_fraction) ::
{Calendar.hour, Calendar.minute, Calendar.second, Calendar.microsecond}
def time_from_day_fraction({parts_in_day, parts_per_day}) do
total_microseconds = div(parts_in_day * @seconds_per_day * @microseconds_per_second, parts_per_day)
{hours, rest_microseconds1} = div_mod(total_microseconds, @seconds_per_hour * @microseconds_per_second)
{minutes, rest_microseconds2} = div_mod(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
{seconds, microseconds} = div_mod(rest_microseconds2, @microseconds_per_second)
{hours, minutes, seconds, {microseconds, 6}}
end
# Converts year, month, day to count of days since 0000-01-01.
@doc false
def date_to_iso_days_days(0, 1, 1) do
0
end
def date_to_iso_days_days(1970, 1, 1) do
719528
end
def date_to_iso_days_days(year, month, day) when year <= 9999 do
:calendar.date_to_gregorian_days(year, month, day)
end
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_iso_days_days(days) when days <= 3652424 do
:calendar.gregorian_days_to_date(days)
end
defp div_mod(int1, int2) do
div = div(int1, int2)
mod = int1 - (div * int2)
{div, mod}
end
@doc """
Returns how many days there are in the given year-month.
## Examples
iex> Calendar.ISO.days_in_month(1900, 1)
31
iex> Calendar.ISO.days_in_month(1900, 2)
28
iex> Calendar.ISO.days_in_month(2000, 2)
29
iex> Calendar.ISO.days_in_month(2001, 2)
28
iex> Calendar.ISO.days_in_month(2004, 2)
29
iex> Calendar.ISO.days_in_month(2004, 4)
30
"""
@spec days_in_month(year, month) :: 28..31
def days_in_month(year, month)
def days_in_month(year, 2) do
if leap_year?(year), do: 29, else: 28
end
def days_in_month(_, month) when month in [4, 6, 9, 11], do: 30
def days_in_month(_, month) when month in 1..12, do: 31
@doc """
Returns if the given year is a leap year.
@@ -181,122 +52,62 @@ defmodule Calendar.ISO do
false
"""
@spec leap_year?(year) :: boolean()
def leap_year?(year) when is_integer(year) and year >= 0 do
rem(year, 4) === 0 and (rem(year, 100) > 0 or rem(year, 400) === 0)
end
@doc """
Calculates the day of the week from the given `year`, `month`, and `day`.
Converts the given structure into a string.
It is an integer from 1 to 7, where 1 is Monday and 7 is Sunday.
## Examples
iex> Calendar.ISO.day_of_week(2016, 10, 31)
1
iex> Calendar.ISO.day_of_week(2016, 11, 01)
2
iex> Calendar.ISO.day_of_week(2016, 11, 02)
3
iex> Calendar.ISO.day_of_week(2016, 11, 03)
4
iex> Calendar.ISO.day_of_week(2016, 11, 04)
5
iex> Calendar.ISO.day_of_week(2016, 11, 05)
6
iex> Calendar.ISO.day_of_week(2016, 11, 06)
7
It uses the ISO8601 standard except for DateTime where the
timezone information is added between brackets.
"""
@spec day_of_week(year, month, day) :: 1..7
def day_of_week(year, month, day)
when is_integer(year) and is_integer(month) and is_integer(day) do
:calendar.day_of_the_week(year, month, day)
def to_string(%Date{year: year, month: month, day: day}) do
date_to_string(year, month, day)
end
@doc """
Converts the given time into a string.
"""
def time_to_string(hour, minute, second, microsecond, format \\ :extended)
def time_to_string(hour, minute, second, {_, 0}, format) do
time_to_string_format(hour, minute, second, format)
def to_string(%Time{hour: hour, minute: minute, second: second, microsecond: microsecond}) do
time_to_string(hour, minute, second, microsecond)
end
def time_to_string(hour, minute, second, {microsecond, precision}, format) do
time_to_string_format(hour, minute, second, format) <>
"." <> (microsecond |> zero_pad(6) |> binary_part(0, precision))
end
defp time_to_string_format(hour, minute, second, :extended) do
zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2) <> ":" <> zero_pad(second, 2)
end
defp time_to_string_format(hour, minute, second, :basic) do
zero_pad(hour, 2) <> zero_pad(minute, 2) <> zero_pad(second, 2)
end
@doc """
Converts the given date into a string.
"""
def date_to_string(year, month, day) do
zero_pad(year, 4) <> "-" <> zero_pad(month, 2) <> "-" <> zero_pad(day, 2)
end
defp date_to_string(year, month, day, :extended), do: date_to_string(year, month, day)
defp date_to_string(year, month, day, :basic) do
zero_pad(year, 4) <> zero_pad(month, 2) <> zero_pad(day, 2)
end
@doc """
Converts the datetime (without time zone) into a string.
"""
def naive_datetime_to_string(year, month, day, hour, minute, second, microsecond) do
def to_string(%NaiveDateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
date_to_string(year, month, day) <> " " <> time_to_string(hour, minute, second, microsecond)
end
@doc """
Convers the datetime (with time zone) into a string.
"""
def datetime_to_string(year, month, day, hour, minute, second, microsecond,
time_zone, zone_abbr, utc_offset, std_offset) do
def to_string(%DateTime{year: year, month: month, day: day, zone_abbr: zone_abbr,
hour: hour, minute: minute, second: second, microsecond: microsecond,
utc_offset: utc_offset, std_offset: std_offset, time_zone: time_zone}) do
date_to_string(year, month, day) <> " " <>
time_to_string(hour, minute, second, microsecond) <>
offset_to_string(utc_offset, std_offset, time_zone) <>
zone_to_string(utc_offset, std_offset, zone_abbr, time_zone)
end
def valid_date?(year, month, day) do
year <= 9999 and :calendar.valid_date(year, month, day)
defp date_to_string(year, month, day) do
zero_pad(year, 4) <> "-" <> zero_pad(month, 2) <> "-" <> zero_pad(day, 2)
end
def valid_time?(hour, minute, second, {microsecond, precision}) do
hour in 0..23 and minute in 0..59 and second in 0..60 and
microsecond in 0..999_999 and precision in 0..6
defp time_to_string(hour, minute, second, 0) do
zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2) <> ":" <> zero_pad(second, 2)
end
defp time_to_string(hour, minute, second, {_, 0}) do
time_to_string(hour, minute, second, 0)
end
defp time_to_string(hour, minute, second, {microsecond, precision}) do
time_to_string(hour, minute, second, 0) <> "." <>
(microsecond |> zero_pad(6) |> binary_part(0, precision))
end
def day_rollover_relative_to_midnight_utc() do
{0, 1}
end
defp offset_to_string(utc, std, zone, format \\ :extended)
defp offset_to_string(0, 0, "Etc/UTC", _format), do: "Z"
defp offset_to_string(utc, std, _zone, format) do
total = utc + std
defp offset_to_string(0, 0, "Etc/UTC"), do: "Z"
defp offset_to_string(utc, std, _zone) do
total = utc + std
second = abs(total)
minute = second |> rem(3600) |> div(60)
hour = div(second, 3600)
format_offset(total, hour, minute, format)
end
defp format_offset(total, hour, minute, :extended) do
hour = second |> div(3600)
sign(total) <> zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2)
end
defp format_offset(total, hour, minute, :basic) do
sign(total) <> zero_pad(hour, 2) <> zero_pad(minute, 2)
end
defp zone_to_string(0, 0, _abbr, "Etc/UTC"), do: ""
defp zone_to_string(_, _, abbr, zone), do: " " <> abbr <> " " <> zone
@@ -311,52 +122,25 @@ defmodule Calendar.ISO do
## Helpers
@doc false
def from_unix(integer, unit) when is_integer(integer) do
total = System.convert_time_unit(integer, unit, :microsecond)
if total in @unix_range_microseconds do
microseconds = Integer.mod(total, @microseconds_per_second)
seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
precision = precision_for_unit(unit)
{date, time} = :calendar.gregorian_seconds_to_datetime(seconds)
{:ok, date, time, {microseconds, precision}}
else
{:error, :invalid_unix_time}
end
def to_iso8601(%Date{year: year, month: month, day: day}) do
date_to_string(year, month, day)
end
defp precision_for_unit(unit) do
subsecond = div System.convert_time_unit(1, :second, unit), 10
precision_for_unit(subsecond, 0)
def to_iso8601(%Time{hour: hour, minute: minute, second: second, microsecond: microsecond}) do
time_to_string(hour, minute, second, microsecond)
end
defp precision_for_unit(0, precision),
do: precision
defp precision_for_unit(_, 6),
do: 6
defp precision_for_unit(number, precision),
do: precision_for_unit(div(number, 10), precision + 1)
@doc false
def date_to_iso8601(year, month, day, format \\ :extended) do
date_to_string(year, month, day, format)
def to_iso8601(%NaiveDateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
date_to_string(year, month, day) <> "T" <> time_to_string(hour, minute, second, microsecond)
end
@doc false
def time_to_iso8601(hour, minute, second, microsecond, format \\ :extended) do
time_to_string(hour, minute, second, microsecond, format)
end
@doc false
def naive_datetime_to_iso8601(year, month, day, hour, minute, second, microsecond, format \\ :extended) do
date_to_string(year, month, day, format) <> "T" <> time_to_string(hour, minute, second, microsecond, format)
end
@doc false
def datetime_to_iso8601(year, month, day, hour, minute, second, microsecond,
time_zone, _zone_abbr, utc_offset, std_offset, format \\ :extended) do
date_to_string(year, month, day, format) <> "T" <>
time_to_string(hour, minute, second, microsecond, format) <>
offset_to_string(utc_offset, std_offset, time_zone, format)
def to_iso8601(%DateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond,
utc_offset: utc_offset, std_offset: std_offset, time_zone: time_zone}) do
date_to_string(year, month, day) <> "T" <>
time_to_string(hour, minute, second, microsecond) <>
offset_to_string(utc_offset, std_offset, time_zone)
end
@doc false
@@ -371,17 +155,12 @@ defmodule Calendar.ISO do
{{String.to_integer(binary_part(microsecond, 0, 6)), 6}, rest}
end
end
def parse_microsecond("," <> rest) do
parse_microsecond("." <> rest)
end
def parse_microsecond(rest) do
{{0, 0}, rest}
end
defp parse_microsecond(<<head, tail::binary>>, precision, acc) when head in ?0..?9,
do: parse_microsecond(tail, precision + 1, <<acc::binary, head>>)
defp parse_microsecond(<<h, t::binary>>, precision, acc) when h in ?0..?9,
do: parse_microsecond(t, precision + 1, <<acc::binary, h>>)
defp parse_microsecond(rest, precision, acc),
do: {acc, precision, rest}
@@ -396,14 +175,6 @@ defmodule Calendar.ISO do
do: parse_offset(1, hour, min, rest)
def parse_offset(<<?-, hour::2-bytes, ?:, min::2-bytes, rest::binary>>),
do: parse_offset(-1, hour, min, rest)
def parse_offset(<<?+, hour::2-bytes, min::2-bytes, rest::binary>>),
do: parse_offset(1, hour, min, rest)
def parse_offset(<<?-, hour::2-bytes, min::2-bytes, rest::binary>>),
do: parse_offset(-1, hour, min, rest)
def parse_offset(<<?+, hour::2-bytes, rest::binary>>),
do: parse_offset(1, hour, "00", rest)
def parse_offset(<<?-, hour::2-bytes, rest::binary>>),
do: parse_offset(-1, hour, "00", rest)
def parse_offset(_),
do: :error
@@ -415,34 +186,4 @@ defmodule Calendar.ISO do
_ -> :error
end
end
@doc false
def iso_days_to_unit({days, {parts, ppd}}, unit) do
day_microseconds = days * @seconds_per_day * @microseconds_per_second
microseconds = div(parts * @seconds_per_day * @microseconds_per_second, ppd)
System.convert_time_unit(day_microseconds + microseconds, :microsecond, unit)
end
@doc false
def add_day_fraction_to_iso_days({days, {parts, ppd}}, add, ppd) do
normalize_iso_days(days, parts + add, ppd)
end
def add_day_fraction_to_iso_days({days, {parts, ppd}}, add, add_ppd) do
parts = parts * add_ppd
add = add * ppd
gcd = Integer.gcd(ppd, add_ppd)
result_parts = div(parts + add, gcd)
result_ppd = div(ppd * add_ppd, gcd)
normalize_iso_days(days, result_parts, result_ppd)
end
defp normalize_iso_days(days, parts, ppd) do
days_offset = div(parts, ppd)
parts = rem(parts, ppd)
if parts < 0 do
{days + days_offset - 1, {parts + ppd, ppd}}
else
{days + days_offset, {parts, ppd}}
end
end
end
-695
View File
@@ -1,695 +0,0 @@
defmodule NaiveDateTime do
@moduledoc """
A NaiveDateTime struct (without a time zone) and functions.
The NaiveDateTime struct contains the fields year, month, day, hour,
minute, second, microsecond and calendar. New naive datetimes can be
built with the `new/2` and `new/7` functions or using the `~N` sigil:
iex> ~N[2000-01-01 23:00:07]
~N[2000-01-01 23:00:07]
The date and time fields in the struct can be accessed directly:
iex> naive = ~N[2000-01-01 23:00:07]
iex> naive.year
2000
iex> naive.second
7
We call them "naive" because this datetime representation does not
have a time zone. This means the datetime may not actually exist in
certain areas in the world even though it is valid.
For example, when daylight saving changes are applied by a region,
the clock typically moves forward or backward by one hour. This means
certain datetimes never occur or may occur more than once. Since
`NaiveDateTime` is not validated against a time zone, such errors
would go unnoticed.
The functions on this module work with the `NaiveDateTime` struct as well
as any struct that contains the same fields as the `NaiveDateTime` struct,
such as `DateTime`. Such functions expect
`t:Calendar.naive_datetime/0` in their typespecs (instead of `t:t/0`).
Developers should avoid creating the NaiveDateTime structs directly
and instead rely on the functions provided by this module as well
as the ones in 3rd party calendar libraries.
## Comparing naive date times
Comparisons in Elixir using `==`, `>`, `<` and similar are structural
and based on the `NaiveDateTime` struct fields. For proper comparison
between naive datetimes, use the `compare/2` function.
## Using epochs
The `add/3` and `diff/3` functions can be used for computing with
date times or retrieving the amount of seconds betweens instants.
For example, if there is an interest in computing the amount of
seconds from the Unix epoch (1970-01-01 00:00:00):
iex> NaiveDateTime.diff(~N[2010-04-17 14:00:00], ~N[1970-01-01 00:00:00])
1271512800
iex> NaiveDateTime.add(~N[1970-01-01 00:00:00], 1271512800)
~N[2010-04-17 14:00:00]
Those functions are optimized to deal with common epochs, such
as the Unix Epoch above or the Gregorian Epoch (0000-01-01 00:00:00).
"""
@enforce_keys [:year, :month, :day, :hour, :minute, :second]
defstruct [:year, :month, :day, :hour, :minute, :second, microsecond: {0, 0}, calendar: Calendar.ISO]
@type t :: %NaiveDateTime{year: Calendar.year, month: Calendar.month, day: Calendar.day,
calendar: Calendar.calendar, hour: Calendar.hour, minute: Calendar.minute,
second: Calendar.second, microsecond: Calendar.microsecond}
@doc """
Returns the current naive datetime in UTC.
Prefer using `DateTime.utc_now/0` when possible as, opposite
to `NaiveDateTime`, it will keep the time zone information.
## Examples
iex> naive_datetime = NaiveDateTime.utc_now()
iex> naive_datetime.year >= 2016
true
"""
@spec utc_now(Calendar.calendar) :: t
def utc_now(calendar \\ Calendar.ISO)
def utc_now(Calendar.ISO) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(:os.system_time, :native)
%NaiveDateTime{year: year, month: month, day: day,
hour: hour, minute: minute, second: second,
microsecond: microsecond, calendar: Calendar.ISO}
end
def utc_now(calendar) do
calendar
|> DateTime.utc_now
|> DateTime.to_naive
end
@doc """
Builds a new ISO naive datetime.
Expects all values to be integers. Returns `{:ok, naive_datetime}`
if each entry fits its appropriate range, returns `{:error, reason}`
otherwise.
## Examples
iex> NaiveDateTime.new(2000, 1, 1, 0, 0, 0)
{:ok, ~N[2000-01-01 00:00:00]}
iex> NaiveDateTime.new(2000, 13, 1, 0, 0, 0)
{:error, :invalid_date}
iex> NaiveDateTime.new(2000, 2, 29, 0, 0, 0)
{:ok, ~N[2000-02-29 00:00:00]}
iex> NaiveDateTime.new(2000, 2, 30, 0, 0, 0)
{:error, :invalid_date}
iex> NaiveDateTime.new(2001, 2, 29, 0, 0, 0)
{:error, :invalid_date}
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 59, {0, 1})
{:ok, ~N[2000-01-01 23:59:59.0]}
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 59, 999_999)
{:ok, ~N[2000-01-01 23:59:59.999999]}
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 60, 999_999)
{:ok, ~N[2000-01-01 23:59:60.999999]}
iex> NaiveDateTime.new(2000, 1, 1, 24, 59, 59, 999_999)
{:error, :invalid_time}
iex> NaiveDateTime.new(2000, 1, 1, 23, 60, 59, 999_999)
{:error, :invalid_time}
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 61, 999_999)
{:error, :invalid_time}
iex> NaiveDateTime.new(2000, 1, 1, 23, 59, 59, 1_000_000)
{:error, :invalid_time}
"""
@spec new(Calendar.year, Calendar.month, Calendar.day,
Calendar.hour, Calendar.minute, Calendar.second, Calendar.microsecond, Calendar.calendar) ::
{:ok, t} | {:error, atom}
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
with {:ok, date} <- Date.new(year, month, day, calendar),
{:ok, time} <- Time.new(hour, minute, second, microsecond, calendar),
do: new(date, time)
end
@doc """
Builds a naive datetime from date and time structs.
## Examples
iex> NaiveDateTime.new(~D[2010-01-13], ~T[23:00:07.005])
{:ok, ~N[2010-01-13 23:00:07.005]}
"""
@spec new(Date.t, Time.t) :: {:ok, t}
def new(date, time)
def new(%Date{calendar: calendar, year: year, month: month, day: day},
%Time{calendar: calendar, hour: hour, minute: minute, second: second, microsecond: microsecond}) do
{:ok, %NaiveDateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}}
end
@doc """
Adds a specified amount of time to a `NaiveDateTime`.
Accepts an `integer` in any `unit` available from `t:System.time_unit/0`.
Negative values will be move backwards in time.
This operation is only possible if both calendars are convertible to `Calendar.ISO`.
## Examples
# adds seconds by default
iex> NaiveDateTime.add(~N[2014-10-02 00:29:10], 2)
~N[2014-10-02 00:29:12]
# accepts negative offsets
iex> NaiveDateTime.add(~N[2014-10-02 00:29:10], -2)
~N[2014-10-02 00:29:08]
# can work with other units
iex> NaiveDateTime.add(~N[2014-10-02 00:29:10], 2_000, :millisecond)
~N[2014-10-02 00:29:12]
# keeps the same precision
iex> NaiveDateTime.add(~N[2014-10-02 00:29:10.021], 21, :second)
~N[2014-10-02 00:29:31.021]
# changes below the precision will not be visible
iex> hidden = NaiveDateTime.add(~N[2014-10-02 00:29:10], 21, :millisecond)
iex> hidden.microsecond # ~N[2014-10-02 00:29:10]
{21000, 0}
# from Gregorian seconds
iex> NaiveDateTime.add(~N[0000-01-01 00:00:00], 63579428950)
~N[2014-10-02 00:29:10]
"""
@spec add(t, integer, System.time_unit) :: t
def add(%NaiveDateTime{microsecond: {_, precision}, calendar: calendar} = naive_datetime,
integer, unit \\ :second) when is_integer(integer) do
ppd = System.convert_time_unit(86400, :second, unit)
naive_datetime
|> to_iso_days()
|> Calendar.ISO.add_day_fraction_to_iso_days(integer, ppd)
|> from_iso_days(calendar, precision)
end
@doc """
Subtracts `naive_datetime2` from `naive_datetime1`.
The answer can be returned in any `unit` available from `t:System.time_unit/0`.
This function returns the difference in seconds where seconds are measured
according to `Calendar.ISO`.
## Examples
iex> NaiveDateTime.diff(~N[2014-10-02 00:29:12], ~N[2014-10-02 00:29:10])
2
iex> NaiveDateTime.diff(~N[2014-10-02 00:29:12], ~N[2014-10-02 00:29:10], :microsecond)
2_000_000
iex> NaiveDateTime.diff(~N[2014-10-02 00:29:10.042], ~N[2014-10-02 00:29:10.021], :millisecond)
21
iex> NaiveDateTime.diff(~N[2014-10-02 00:29:10], ~N[2014-10-02 00:29:12])
-2
# to Gregorian seconds
iex> NaiveDateTime.diff(~N[2014-10-02 00:29:10], ~N[0000-01-01 00:00:00])
63579428950
"""
@spec diff(t, t, System.time_unit) :: integer
def diff(%NaiveDateTime{} = naive_datetime1,
%NaiveDateTime{} = naive_datetime2,
unit \\ :second) do
if not Calendar.compatible_calendars?(naive_datetime1.calendar, naive_datetime2.calendar) do
raise ArgumentError, "cannot calculate the difference between #{inspect naive_datetime1} and #{inspect naive_datetime2} because their calendars are not compatible and thus the result would be ambiguous"
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
end
@doc """
Converts a `NaiveDateTime` into a `Date`.
Because `Date` does not hold time information,
data will be lost during the conversion.
## Examples
iex> NaiveDateTime.to_date(~N[2002-01-13 23:00:07])
~D[2002-01-13]
"""
@spec to_date(t) :: Date.t
def to_date(%NaiveDateTime{year: year, month: month, day: day, calendar: calendar}) do
%Date{year: year, month: month, day: day, calendar: calendar}
end
@doc """
Converts a `NaiveDateTime` into `Time`.
Because `Time` does not hold date information,
data will be lost during the conversion.
## Examples
iex> NaiveDateTime.to_time(~N[2002-01-13 23:00:07])
~T[23:00:07]
"""
@spec to_time(t) :: Time.t
def to_time(%NaiveDateTime{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}) do
%Time{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}
end
@doc """
Converts the given naive datetime to a string according to its calendar.
### Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
"2000-02-28 23:00:13"
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13.001])
"2000-02-28 23:00:13.001"
This function can also be used to convert a DateTime to a string without
the time zone information:
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> NaiveDateTime.to_string(dt)
"2000-02-29 23:00:07"
"""
@spec to_string(Calendar.naive_datetime) :: String.t
def to_string(naive_datetime)
def to_string(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
end
@doc """
Parses the extended "Date and time of day" format described by
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
Timezone offset may be included in the string but they will be
simply discarded as such information is not included in naive date
times.
As specified in the standard, the separator "T" may be omitted if
desired as there is no ambiguity within this function.
Time representations with reduced accuracy are not supported.
Note that while ISO8601 allows datetimes to specify 24:00:00 as the
zero hour of the next day, this notation is not supported by Elixir.
## Examples
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07")
{:ok, ~N[2015-01-23 23:50:07]}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07")
{:ok, ~N[2015-01-23 23:50:07]}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07Z")
{:ok, ~N[2015-01-23 23:50:07]}
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07.0")
{:ok, ~N[2015-01-23 23:50:07.0]}
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07,0123456")
{:ok, ~N[2015-01-23 23:50:07.012345]}
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07.0123456")
{:ok, ~N[2015-01-23 23:50:07.012345]}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123Z")
{:ok, ~N[2015-01-23 23:50:07.123]}
iex> NaiveDateTime.from_iso8601("2015-01-23P23:50:07")
{:error, :invalid_format}
iex> NaiveDateTime.from_iso8601("2015:01:23 23-50-07")
{:error, :invalid_format}
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:07A")
{:error, :invalid_format}
iex> NaiveDateTime.from_iso8601("2015-01-23 23:50:61")
{:error, :invalid_time}
iex> NaiveDateTime.from_iso8601("2015-01-32 23:50:07")
{:error, :invalid_date}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123+02:30")
{:ok, ~N[2015-01-23 23:50:07.123]}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123+00:00")
{:ok, ~N[2015-01-23 23:50:07.123]}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123-02:30")
{:ok, ~N[2015-01-23 23:50:07.123]}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123-00:00")
{:error, :invalid_format}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123-00:60")
{:error, :invalid_format}
iex> NaiveDateTime.from_iso8601("2015-01-23T23:50:07.123-24:00")
{:error, :invalid_format}
"""
@spec from_iso8601(String.t, Calendar.calendar) :: {:ok, t} | {:error, atom}
def from_iso8601(string, calendar \\ Calendar.ISO)
def from_iso8601(<<year::4-bytes, ?-, month::2-bytes, ?-, day::2-bytes, sep,
hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes, rest::binary>>, calendar) when sep in [?\s, ?T] do
with {year, ""} <- Integer.parse(year),
{month, ""} <- Integer.parse(month),
{day, ""} <- Integer.parse(day),
{hour, ""} <- Integer.parse(hour),
{min, ""} <- Integer.parse(min),
{sec, ""} <- Integer.parse(sec),
{microsec, rest} <- Calendar.ISO.parse_microsecond(rest),
{_offset, ""} <- Calendar.ISO.parse_offset(rest) do
with {:ok, utc_date} <- new(year, month, day, hour, min, sec, microsec, Calendar.ISO),
do: convert(utc_date, calendar)
else
_ -> {:error, :invalid_format}
end
end
def from_iso8601(<<_::binary>>, _calendar) do
{:error, :invalid_format}
end
@doc """
Parses the extended "Date and time of day" format described by
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
Raises if the format is invalid.
## Examples
iex> NaiveDateTime.from_iso8601!("2015-01-23T23:50:07.123Z")
~N[2015-01-23 23:50:07.123]
iex> NaiveDateTime.from_iso8601!("2015-01-23T23:50:07,123Z")
~N[2015-01-23 23:50:07.123]
iex> NaiveDateTime.from_iso8601!("2015-01-23P23:50:07")
** (ArgumentError) cannot parse "2015-01-23P23:50:07" as naive datetime, reason: :invalid_format
"""
@spec from_iso8601!(String.t, Calendar.calendar) :: t | no_return
def from_iso8601!(string, calendar \\ Calendar.ISO) do
case from_iso8601(string, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot parse #{inspect string} as naive datetime, reason: #{inspect reason}"
end
end
@doc """
Converts the given naive datetime to
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
By default, `NaiveDateTime.to_iso8601/2` returns naive datetimes formatted in the "extended"
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
Only supports converting naive datetimes which are in the ISO calendar,
attempting to convert naive datetimes from other calendars will raise.
### Examples
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
"2000-02-28T23:00:13"
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13.001])
"2000-02-28T23:00:13.001"
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13.001], :basic)
"20000228T230013.001"
This function can also be used to convert a DateTime to ISO8601 without
the time zone information:
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> NaiveDateTime.to_iso8601(dt)
"2000-02-29T23:00:07"
"""
@spec to_iso8601(Calendar.naive_datetime, :basic | :extended) :: String.t
def to_iso8601(naive_datetime, format \\ :extended)
def to_iso8601(%{year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond, calendar:
Calendar.ISO}, format) when format in [:basic, :extended] do
Calendar.ISO.naive_datetime_to_iso8601(year, month, day, hour, minute, second, microsecond, format)
end
def to_iso8601(%{year: _, month: _, day: _,
hour: _, minute: _, second: _, microsecond: _, calendar: _} = naive_datetime, format) when format in [:basic, :extended] do
naive_datetime
|> convert!(Calendar.ISO)
|> to_iso8601(format)
end
def to_iso8601(_date, format) do
raise ArgumentError, "NaiveDateTime.to_iso8601/2 expects format to be :extended or :basic, got: #{inspect format}"
end
@doc """
Converts a `NaiveDateTime` struct to an Erlang datetime tuple.
Only supports converting naive datetimes which are in the ISO calendar,
attempting to convert naive datetimes from other calendars will raise.
WARNING: Loss of precision may occur, as Erlang time tuples only store
hour/minute/second.
## Examples
iex> NaiveDateTime.to_erl(~N[2000-01-01 13:30:15])
{{2000, 1, 1}, {13, 30, 15}}
This function can also be used to convert a DateTime to a erl format
without the time zone information:
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> NaiveDateTime.to_erl(dt)
{{2000, 2, 29}, {23, 00, 07}}
"""
@spec to_erl(t) :: :calendar.datetime
def to_erl(naive_datetime)
@spec to_erl(Calendar.time) :: :calendar.time
def to_erl(%{calendar: _, year: _, month: _, day: _,
hour: _, minute: _, second: _} = naive_datetime) do
%{year: year, month: month, day: day,
hour: hour, minute: minute, second: second} = convert!(naive_datetime, Calendar.ISO)
{{year, month, day}, {hour, minute, second}}
end
@doc """
Converts an Erlang datetime tuple to a `NaiveDateTime` struct.
Attempting to convert an invalid ISO calendar date will produce an error tuple.
## Examples
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
{:ok, ~N[2000-01-01 13:30:15]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
{:ok, ~N[2000-01-01 13:30:15.005]}
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
{:error, :invalid_date}
iex> NaiveDateTime.from_erl({{2000, 13, 1},{13, 30, 15}})
{:error, :invalid_date}
"""
@spec from_erl(:calendar.datetime, Calendar.microsecond) :: {:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
def from_erl({{year, month, day}, {hour, minute, second}}, microsecond, calendar) do
with {:ok, utc_date} <- new(year, month, day, hour, minute, second, microsecond),
do: convert(utc_date, calendar)
end
@doc """
Converts an Erlang datetime tuple to a `NaiveDateTime` struct.
Raises if the datetime is invalid.
Attempting to convert an invalid ISO calendar date will produce an error tuple.
## Examples
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
~N[2000-01-01 13:30:15]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
~N[2000-01-01 13:30:15.005]
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
"""
@spec from_erl!(:calendar.datetime, Calendar.microsecond) :: t | no_return
def from_erl!(tuple, microsecond \\ {0, 0}) do
case from_erl(tuple, microsecond) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot convert #{inspect tuple} to naive datetime, reason: #{inspect reason}"
end
end
@doc """
Compares two `NaiveDateTime` structs.
Returns `:gt` if first is later than the second
and `:lt` for vice versa. If the two NaiveDateTime
are equal `:eq` is returned.
## Examples
iex> NaiveDateTime.compare(~N[2016-04-16 13:30:15], ~N[2016-04-28 16:19:25])
:lt
iex> NaiveDateTime.compare(~N[2016-04-16 13:30:15.1], ~N[2016-04-16 13:30:15.01])
:gt
This function can also be used to compare a DateTime without
the time zone information:
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
iex> NaiveDateTime.compare(dt, ~N[2000-02-29 23:00:07])
:eq
iex> NaiveDateTime.compare(dt, ~N[2000-01-29 23:00:07])
:gt
iex> NaiveDateTime.compare(dt, ~N[2000-03-29 23:00:07])
:lt
"""
@spec compare(Calendar.naive_datetime, Calendar.naive_datetime) :: :lt | :eq | :gt
def compare(%{calendar: calendar1} = naive_datetime1, %{calendar: calendar2} = naive_datetime2) do
if Calendar.compatible_calendars?(calendar1, calendar2) do
case {to_iso_days(naive_datetime1), to_iso_days(naive_datetime2)} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
end
else
raise ArgumentError, """
cannot compare #{inspect naive_datetime1} with #{inspect naive_datetime2}.
This comparison would be ambiguous as their calendars have incompatible day rollover moments.
Specify an exact time of day (using `DateTime`s) to resolve this ambiguity
"""
end
end
@doc """
Converts the given `naive_datetime` from one calendar to another.
If it is not possible to convert unambiguously between the calendars
(see `Calendar.compatible_calendars?/2`), an `{:error, :incompatible_calendars}` tuple
is returned.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
{:ok, %NaiveDateTime{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1,
hour: 13, minute: 30, second: 15, microsecond: {0, 0}}}
"""
@spec convert(Calendar.naive_datetime, Calendar.calendar) :: {:ok, t} | {:error, :incompatible_calendars}
def convert(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}, calendar) do
{:ok, %NaiveDateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}}
end
def convert(%{calendar: ndt_calendar, microsecond: {_, precision}} = naive_datetime, calendar) do
if Calendar.compatible_calendars?(ndt_calendar, calendar) do
result_naive_datetime =
naive_datetime
|> to_iso_days
|> from_iso_days(calendar, precision)
{:ok, result_naive_datetime}
else
{:error, :incompatible_calendars}
end
end
@doc """
Converts the given `naive_datetime` from one calendar to another.
If it is not possible to convert unambiguously between the calendars
(see `Calendar.compatible_calendars?/2`), an ArgumentError is raised.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
%NaiveDateTime{calendar: Calendar.Holocene, year: 12000, month: 1, day: 1,
hour: 13, minute: 30, second: 15, microsecond: {0, 0}}
"""
@spec convert!(Calendar.naive_datetime, Calendar.calendar) :: t
def convert!(naive_datetime, calendar) do
case convert(naive_datetime, calendar) do
{:ok, value} ->
value
{:error, :incompatible_calendars} ->
raise ArgumentError, "cannot convert #{inspect naive_datetime} to target calendar #{inspect calendar}, reason: #{inspect naive_datetime.calendar} and #{inspect calendar} have different day rollover moments, making this conversion ambiguous"
end
end
## Helpers
defp to_iso_days(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
end
defp from_iso_days(iso_days, calendar, precision) do
{year, month, day, hour, minute, second, {microsecond, _}} =
calendar.naive_datetime_from_iso_days(iso_days)
%NaiveDateTime{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: {microsecond, precision}}
end
defimpl String.Chars do
def to_string(%{calendar: calendar, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}) do
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
end
end
defimpl Inspect do
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day,
hour: hour, minute: minute, second: second, microsecond: microsecond}, _) do
formatted = Calendar.ISO.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
"~N[" <> formatted <> "]"
end
def inspect(naive, opts) do
Inspect.Any.inspect(naive, opts)
end
end
end
-491
View File
@@ -1,491 +0,0 @@
defmodule Time do
@moduledoc """
A Time struct and functions.
The Time struct contains the fields hour, minute, second and microseconds.
New times can be built with the `new/4` function or using the `~T`
sigil:
iex> ~T[23:00:07.001]
~T[23:00:07.001]
Both `new/4` and sigil return a struct where the time fields can
be accessed directly:
iex> time = ~T[23:00:07.001]
iex> time.hour
23
iex> time.microsecond
{1000, 3}
The functions on this module work with the `Time` struct as well
as any struct that contains the same fields as the `Time` struct,
such as `NaiveDateTime` and `DateTime`. Such functions expect
`t:Calendar.time/0` in their typespecs (instead of `t:t/0`).
Developers should avoid creating the Time structs directly
and instead rely on the functions provided by this module as well
as the ones in 3rd party calendar libraries.
## Comparing times
Comparisons in Elixir using `==`, `>`, `<` and similar are structural
and based on the `Time` struct fields. For proper comparison between
times, use the `compare/2` function.
"""
@enforce_keys [:hour, :minute, :second]
defstruct [:hour, :minute, :second, microsecond: {0, 0}, calendar: Calendar.ISO]
@type t :: %Time{hour: Calendar.hour, minute: Calendar.minute,
second: Calendar.second, microsecond: Calendar.microsecond, calendar: Calendar.calendar}
@doc """
Returns the current time in UTC.
## Examples
iex> time = Time.utc_now()
iex> time.hour >= 0
true
"""
@spec utc_now(Calendar.calendar) :: t
def utc_now(calendar \\ Calendar.ISO) do
{:ok, _, {hour, minute, second}, microsecond} = Calendar.ISO.from_unix(:os.system_time, :native)
iso_time = %Time{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: Calendar.ISO}
convert!(iso_time, calendar)
end
@doc """
Builds a new time.
Expects all values to be integers. Returns `{:ok, time}` if each
entry fits its appropriate range, returns `{:error, reason}` otherwise.
Note a time may have 60 seconds in case of leap seconds. Microseconds
can also be given with a precision, which must be an integer between
0 and 6.
## Examples
iex> Time.new(0, 0, 0, 0)
{:ok, ~T[00:00:00.000000]}
iex> Time.new(23, 59, 59, 999_999)
{:ok, ~T[23:59:59.999999]}
iex> Time.new(23, 59, 60, 999_999)
{:ok, ~T[23:59:60.999999]}
# Time with microseconds and their precision
iex> Time.new(23, 59, 60, {10_000, 2})
{:ok, ~T[23:59:60.01]}
iex> Time.new(24, 59, 59, 999_999)
{:error, :invalid_time}
iex> Time.new(23, 60, 59, 999_999)
{:error, :invalid_time}
iex> Time.new(23, 59, 61, 999_999)
{:error, :invalid_time}
iex> Time.new(23, 59, 59, 1_000_000)
{:error, :invalid_time}
# Invalid precision
Time.new(23, 59, 59, {999_999, 10})
{:error, :invalid_time}
"""
@spec new(Calendar.hour, Calendar.minute, Calendar.second, Calendar.microsecond, Calendar.calendar) ::
{:ok, t} | {:error, atom}
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
def new(hour, minute, second, microsecond, calendar) when is_integer(microsecond) do
new(hour, minute, second, {microsecond, 6}, calendar)
end
def new(hour, minute, second, {microsecond, precision}, calendar)
when is_integer(hour) and is_integer(minute) and is_integer(second) and
is_integer(microsecond) and is_integer(precision) do
case calendar.valid_time?(hour, minute, second, {microsecond, precision}) do
true ->
{:ok, %Time{hour: hour, minute: minute, second: second, microsecond: {microsecond, precision}, calendar: calendar}}
false ->
{:error, :invalid_time}
end
end
@doc """
Converts the given `time` to a string.
### Examples
iex> Time.to_string(~T[23:00:00])
"23:00:00"
iex> Time.to_string(~T[23:00:00.001])
"23:00:00.001"
iex> Time.to_string(~T[23:00:00.123456])
"23:00:00.123456"
iex> Time.to_string(~N[2015-01-01 23:00:00.001])
"23:00:00.001"
iex> Time.to_string(~N[2015-01-01 23:00:00.123456])
"23:00:00.123456"
"""
@spec to_string(Calendar.time) :: String.t
def to_string(time)
def to_string(%{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}) do
calendar.time_to_string(hour, minute, second, microsecond)
end
@doc """
Parses the extended "Local time" format described by
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
Timezone offset may be included in the string but they will be
simply discarded as such information is not included in times.
As specified in the standard, the separator "T" may be omitted if
desired as there is no ambiguity within this function.
Time representations with reduced accuracy are not supported.
Note that while ISO8601 allows times to specify 24:00:00 as the
zero hour of the next day, this notation is not supported by Elixir.
## Examples
iex> Time.from_iso8601("23:50:07")
{:ok, ~T[23:50:07]}
iex> Time.from_iso8601("23:50:07Z")
{:ok, ~T[23:50:07]}
iex> Time.from_iso8601("T23:50:07Z")
{:ok, ~T[23:50:07]}
iex> Time.from_iso8601("23:50:07,0123456")
{:ok, ~T[23:50:07.012345]}
iex> Time.from_iso8601("23:50:07.0123456")
{:ok, ~T[23:50:07.012345]}
iex> Time.from_iso8601("23:50:07.123Z")
{:ok, ~T[23:50:07.123]}
iex> Time.from_iso8601("2015:01:23 23-50-07")
{:error, :invalid_format}
iex> Time.from_iso8601("23:50:07A")
{:error, :invalid_format}
iex> Time.from_iso8601("23:50:07.")
{:error, :invalid_format}
iex> Time.from_iso8601("23:50:61")
{:error, :invalid_time}
"""
@spec from_iso8601(String.t) :: {:ok, t} | {:error, atom}
def from_iso8601(string, calendar \\ Calendar.ISO)
def from_iso8601(<<?T, h, rest::binary>>, calendar) when h in ?0..?9 do
from_iso8601(<<h, rest::binary>>, calendar)
end
def from_iso8601(<<hour::2-bytes, ?:, min::2-bytes, ?:, sec::2-bytes, rest::binary>>, calendar) do
with {hour, ""} <- Integer.parse(hour),
{min, ""} <- Integer.parse(min),
{sec, ""} <- Integer.parse(sec),
{microsec, rest} <- Calendar.ISO.parse_microsecond(rest),
{_offset, ""} <- Calendar.ISO.parse_offset(rest) do
with {:ok, utc_time} <- new(hour, min, sec, microsec, Calendar.ISO),
do: convert(utc_time, calendar)
else
_ -> {:error, :invalid_format}
end
end
def from_iso8601(<<_::binary>>, _calendar) do
{:error, :invalid_format}
end
@doc """
Parses the extended "Local time" format described by
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
Raises if the format is invalid.
## Examples
iex> Time.from_iso8601!("23:50:07,123Z")
~T[23:50:07.123]
iex> Time.from_iso8601!("23:50:07.123Z")
~T[23:50:07.123]
iex> Time.from_iso8601!("2015:01:23 23-50-07")
** (ArgumentError) cannot parse "2015:01:23 23-50-07" as time, reason: :invalid_format
"""
@spec from_iso8601!(String.t) :: t
def from_iso8601!(string) do
case from_iso8601(string) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot parse #{inspect string} as time, reason: #{inspect reason}"
end
end
@doc """
Converts the given time to
[ISO 8601:2004](https://en.wikipedia.org/wiki/ISO_8601).
By default, `Time.to_iso8601/2` returns times formatted in the "extended"
format, for human readability. It also supports the "basic" format through
passing the `:basic` option.
### Examples
iex> Time.to_iso8601(~T[23:00:13])
"23:00:13"
iex> Time.to_iso8601(~T[23:00:13.001])
"23:00:13.001"
iex> Time.to_iso8601(~T[23:00:13.001], :basic)
"230013.001"
iex> Time.to_iso8601(~N[2010-04-17 23:00:13])
"23:00:13"
"""
@spec to_iso8601(Calendar.time, :extended | :basic) :: String.t
def to_iso8601(time, format \\ :extended) when format in [:extended, :basic] do
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = convert!(time, Calendar.ISO)
Calendar.ISO.time_to_iso8601(hour, minute, second, microsecond, format)
end
@doc """
Converts given `time` to an Erlang time tuple.
WARNING: Loss of precision may occur, as Erlang time tuples
only contain hours/minutes/seconds.
## Examples
iex> Time.to_erl(~T[23:30:15.999])
{23, 30, 15}
iex> Time.to_erl(~N[2010-04-17 23:30:15.999])
{23, 30, 15}
"""
@spec to_erl(Calendar.time) :: :calendar.time
def to_erl(time) do
%{hour: hour, minute: minute, second: second} = convert!(time, Calendar.ISO)
{hour, minute, second}
end
@doc """
Converts an Erlang time tuple to a `Time` struct.
## Examples
iex> Time.from_erl({23, 30, 15}, {5000, 3})
{:ok, ~T[23:30:15.005]}
iex> Time.from_erl({24, 30, 15})
{:error, :invalid_time}
"""
@spec from_erl(:calendar.time, Calendar.microsecond, Calendar.calendar) :: {:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
def from_erl({hour, minute, second}, microsecond, calendar) do
with {:ok, time} <- new(hour, minute, second, microsecond, Calendar.ISO),
do: convert(time, calendar)
end
@doc """
Converts an Erlang time tuple to a `Time` struct.
## Examples
iex> Time.from_erl!({23, 30, 15})
~T[23:30:15]
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
~T[23:30:15.005]
iex> Time.from_erl!({24, 30, 15})
** (ArgumentError) cannot convert {24, 30, 15} to time, reason: :invalid_time
"""
@spec from_erl!(:calendar.time, Calendar.microsecond, Calendar.calendar) :: t
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case from_erl(tuple, microsecond, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot convert #{inspect tuple} to time, reason: #{inspect reason}"
end
end
@doc """
Compares two time structs.
Returns `:gt` if first time is later than the second
and `:lt` for vice versa. If the two times are equal
`:eq` is returned.
## Examples
iex> Time.compare(~T[16:04:16], ~T[16:04:28])
:lt
iex> Time.compare(~T[16:04:16], ~T[16:04:16])
:eq
iex> Time.compare(~T[16:04:16.01], ~T[16:04:16.001])
:gt
This function can also be used to compare across more
complex calendar types by considering only the time fields:
iex> Time.compare(~N[1900-01-01 16:04:16], ~N[2015-01-01 16:04:16])
:eq
iex> Time.compare(~N[2015-01-01 16:04:16], ~N[2015-01-01 16:04:28])
:lt
iex> Time.compare(~N[2015-01-01 16:04:16.01], ~N[2000-01-01 16:04:16.001])
:gt
"""
@spec compare(Calendar.time, Calendar.time) :: :lt | :eq | :gt
def compare(%{calendar: calendar, hour: hour1, minute: minute1, second: second1, microsecond: {microsecond1, _}},
%{calendar: calendar, hour: hour2, minute: minute2, second: second2, microsecond: {microsecond2, _}}) do
case {{hour1, minute1, second1, microsecond1}, {hour2, minute2, second2, microsecond2}} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
end
end
def compare(time1, time2) do
{parts1, ppd1} = to_day_fraction(time1)
{parts2, ppd2} = to_day_fraction(time2)
case {parts1 * ppd2, parts2 * ppd1} do
{first, second} when first > second -> :gt
{first, second} when first < second -> :lt
_ -> :eq
end
end
@doc """
Converts given `time` to a different calendar.
Returns `{:ok, time}` if the conversion was successful,
or `{:error, reason}` if it was not, for some reason.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
{:ok, %Time{calendar: Calendar.Holocene, hour: 13, minute: 30, second: 15, microsecond: {0, 0}}}
"""
@spec convert(Calendar.time, Calendar.calendar) :: {:ok, t} | {:error, atom}
def convert(%{calendar: calendar, hour: hour, minute: minute, second: second, microsecond: microsecond}, calendar) do
{:ok, %Time{calendar: calendar, hour: hour, minute: minute, second: second, microsecond: microsecond}}
end
def convert(%{microsecond: {_, precision}} = time, calendar) do
{hour, minute, second, {microsecond, _}} =
time
|> to_day_fraction()
|> calendar.time_from_day_fraction
{:ok, %Time{calendar: calendar, hour: hour, minute: minute, second: second,
microsecond: {microsecond, precision}}}
end
@doc """
Similar to `Time.convert/2`, but raises an `ArgumentError`
if the conversion between the two calendars is not possible.
## Examples
Imagine someone implements `Calendar.Holocene`, a calendar based on the
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
year:
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
%Time{calendar: Calendar.Holocene, hour: 13, minute: 30, second: 15, microsecond: {0, 0}}
"""
@spec convert!(Calendar.time, Calendar.calendar) :: t
def convert!(time, calendar) do
case convert(time, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError, "cannot convert #{inspect time} to target calendar #{inspect calendar}, reason: #{inspect reason}"
end
end
@doc """
Returns the difference between two times, considering only the hour, minute
second and microsecond.
As with the `compare/2` function both `Time` structs and other structures
containing time can be used. If for instance a `NaiveDateTime` or `DateTime`
is passed, only the hour, month, second, and microsecond is considered. Any
additional information about a date or time zone is ignored when calculating
the difference.
The answer can be returned in any `unit` available from
`t:System.time_unit/0`. If the first unit is smaller than
the second, a negative number is returned.
This function returns the difference in seconds where seconds
are measured according to `Calendar.ISO`.
## Examples
iex> Time.diff(~T[00:29:12], ~T[00:29:10])
2
# When passing a `NaiveDateTime` the date part is ignored.
iex> Time.diff(~N[2017-01-01 00:29:12], ~T[00:29:10])
2
# Two `NaiveDateTime` structs could have big differences in the date
# but only the time part is considered.
iex> Time.diff(~N[2017-01-01 00:29:12], (~N[1900-02-03 00:29:10]))
2
iex> Time.diff(~T[00:29:12], ~T[00:29:10], :microsecond)
2_000_000
iex> Time.diff(~T[00:29:10], ~T[00:29:12], :microsecond)
-2_000_000
"""
@spec diff(Calendar.time, Calendar.time, System.time_unit) :: integer
def diff(time1, time2, unit \\ :second) do
fraction1 = to_day_fraction(time1)
fraction2 = to_day_fraction(time2)
Calendar.ISO.iso_days_to_unit({0, fraction1}, unit) -
Calendar.ISO.iso_days_to_unit({0, fraction2}, unit)
end
## Helpers
defp to_day_fraction(%{hour: hour, minute: minute, second: second, microsecond: {_, _} = microsecond, calendar: calendar}) do
calendar.time_to_day_fraction(hour, minute, second, microsecond)
end
defimpl String.Chars do
def to_string(%{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: calendar}) do
calendar.time_to_string(hour, minute, second, microsecond)
end
end
defimpl Inspect do
def inspect(%{hour: hour, minute: minute, second: second, microsecond: microsecond, calendar: Calendar.ISO}, _) do
"~T[" <> Calendar.ISO.time_to_string(hour, minute, second, microsecond) <> "]"
end
def inspect(time, opts) do
Inspect.Any.inspect(time, opts)
end
end
end
+19 -53
View File
@@ -104,15 +104,7 @@ defmodule Code do
The `binding` argument is a keyword list of variable bindings.
The `opts` argument is a keyword list of environment options.
**Warning**: `string` can be any Elixir code and will be executed with
the same privileges as the Erlang VM: this means that such code could
compromise the machine (for example by executing system commands).
Don't use `eval_string/3` with untrusted input (such as strings coming
from the network).
## Options
Options can be:
Those options can be:
* `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts
@@ -156,7 +148,7 @@ defmodule Code do
iex> Code.eval_string("a = a + b", [a: 1, b: 2])
{3, [a: 3, b: 2]}
For convenience, you can pass `__ENV__/0` as the `opts` argument and
For convenience, you can pass `__ENV__` as the `opts` argument and
all imports, requires and aliases defined in the current environment
will be automatically carried over:
@@ -180,12 +172,7 @@ defmodule Code do
@doc """
Evaluates the quoted contents.
**Warning**: Calling this function inside a macro is considered bad
practice as it will attempt to evaluate runtime values at compile time.
Macro arguments are typically transformed by unquoting them into the
returned quoted expressions (instead of evaluated).
See `eval_string/3` for a description of bindings and options.
See `eval_string/3` for a description of arguments and return values.
## Examples
@@ -193,7 +180,7 @@ defmodule Code do
iex> Code.eval_quoted(contents, [a: 1, b: 2], file: __ENV__.file, line: __ENV__.line)
{3, [a: 1, b: 2]}
For convenience, you can pass `__ENV__/0` as the `opts` argument and
For convenience, you can pass `__ENV__` as the `opts` argument and
all options will be automatically extracted from the current environment:
iex> contents = quote(do: var!(a) + var!(b))
@@ -260,9 +247,9 @@ defmodule Code do
## Options
* `:file` - the filename to be used in stacktraces
and the file reported in the `__ENV__/0` macro
and the file reported in the `__ENV__` variable
* `:line` - the line reported in the `__ENV__/0` macro
* `:line` - the line reported in the `__ENV__` variable
* `:existing_atoms_only` - when `true`, raises an error
when non-existing atoms are found by the tokenizer
@@ -341,11 +328,11 @@ defmodule Code do
Accepts `relative_to` as an argument to tell where the file is located.
The return value is the same as that of `load_file/2`. If the file was already
required/loaded, `require_file` doesn't do anything and returns `nil`.
required/loaded, doesn't do anything and returns `nil`.
Notice that if `require_file` is invoked by different processes concurrently,
the first process to invoke `require_file` acquires a lock and the remaining
ones will block until the file is available. I.e., if `require_file` is called
ones will block until the file is available. I.e. if `require_file` is called
N times with a given file, it will be loaded only once. The first process to
call `require_file` will get the list of loaded modules, others will get `nil`.
@@ -403,11 +390,11 @@ defmodule Code do
## Examples
iex> Code.available_compiler_options
[:docs, :debug_info, :ignore_module_conflict, :relative_paths, :warnings_as_errors]
[:docs, :debug_info, :ignore_module_conflict, :warnings_as_errors]
"""
def available_compiler_options do
[:docs, :debug_info, :ignore_module_conflict, :relative_paths, :warnings_as_errors]
[:docs, :debug_info, :ignore_module_conflict, :warnings_as_errors]
end
@doc """
@@ -427,11 +414,7 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, override modules that were
already defined without raising errors, `false` by default
* `:relative_paths` - when `true`, use relative paths in quoted nodes,
warnings and errors generated by the compiler, `true` by default.
Note disabling this option won't affect runtime warnings and errors.
* `:warnings_as_errors` - causes compilation to fail when warnings are
* `:warnings_as_errors` - cause compilation to fail when warnings are
generated
It returns the new list of compiler options.
@@ -446,16 +429,9 @@ defmodule Code do
def compiler_options(opts) do
available = available_compiler_options()
Enum.each(opts, fn({key, value}) ->
cond do
key not in available ->
raise "unknown compiler option: #{inspect(key)}"
not is_boolean(value) ->
raise "compiler option #{inspect(key)} should be a boolean, got: #{inspect(value)}"
true ->
:ok
end
end)
for {k, _} <- opts,
not k in available,
do: raise "unknown compiler options: #{k}"
:elixir_config.update :compiler_options, &Enum.into(opts, &1)
end
@@ -504,7 +480,7 @@ defmodule Code do
module uses this function to check if a specific parser exists for a given
URI scheme.
## `ensure_compiled/1`
## Code.ensure_compiled/1
Elixir also contains an `ensure_compiled/1` function that is a
superset of `ensure_loaded/1`.
@@ -513,13 +489,8 @@ defmodule Code do
you may need to use a module that was not yet compiled, therefore
it can't even be loaded.
When invoked, `ensure_compiled/1` halts the compilation of the caller
until the module given to `ensure_compiled/1` becomes available or
all files for the current project have been compiled. If compilation
finishes and the module is not available, an error tuple is returned.
`ensure_compiled/1` does not apply to dependencies, as dependencies
must be compiled upfront.
`ensure_compiled/1` halts the current process until the
module we are depending on is available.
In most cases, `ensure_loaded/1` is enough. `ensure_compiled/1`
must be used in rare cases, usually involving macros that need to
@@ -534,8 +505,6 @@ defmodule Code do
{:error, :nofile}
"""
@spec ensure_loaded(module) ::
{:module, module} | {:error, :embedded | :badfile | :nofile | :on_load_failure}
def ensure_loaded(module) when is_atom(module) do
:code.ensure_loaded(module)
end
@@ -553,7 +522,7 @@ defmodule Code do
true
"""
def ensure_loaded?(module) when is_atom(module) do
def ensure_loaded?(module) do
match?({:module, ^module}, ensure_loaded(module))
end
@@ -570,8 +539,6 @@ defmodule Code do
Check `ensure_loaded/1` for more information on module loading
and when to use `ensure_loaded/1` or `ensure_compiled/1`.
"""
@spec ensure_compiled(module) ::
{:module, module} | {:error, :embedded | :badfile | :nofile | :on_load_failure}
def ensure_compiled(module) when is_atom(module) do
case :code.ensure_loaded(module) do
{:error, :nofile} = error ->
@@ -592,8 +559,7 @@ defmodule Code do
is already loaded or was successfully loaded and compiled.
Returns `false` otherwise.
"""
@spec ensure_compiled?(module) :: boolean
def ensure_compiled?(module) when is_atom(module) do
def ensure_compiled?(module) do
match?({:module, ^module}, ensure_compiled(module))
end
+9 -40
View File
@@ -24,54 +24,23 @@ defprotocol Collectable do
`Enumerable` protocol. `into/1` can be seen as the opposite of
`Enumerable.reduce/3`. If `Enumerable` is about taking values out,
`Collectable.into/1` is about collecting those values into a structure.
## Examples
To show how to manually use the `Collectable` protocol, let's play with its
implementation for `MapSet`.
iex> {initial_acc, collector_fun} = Collectable.into(MapSet.new())
iex> updated_acc = Enum.reduce([1, 2, 3], initial_acc, fn elem, acc ->
...> collector_fun.(acc, {:cont, elem})
...> end)
iex> collector_fun.(updated_acc, :done)
#MapSet<[1, 2, 3]>
To show how the protocol can be implemented, we can take again a look at the
implementation for `MapSet`. In this implementation "collecting" elements
simply means inserting them in the set through `MapSet.put/2`.
defimpl Collectable do
def into(original) do
collector_fun = fn
set, {:cont, elem} -> MapSet.put(set, elem)
set, :done -> set
_set, :halt -> :ok
end
{original, collector_fun}
end
end
"""
@type command :: {:cont, term} | :done | :halt
@doc """
Returns an initial accumulator and a "collector" function.
Returns a function that collects values alongside
the initial accumulation value.
The returned function receives a term and a command and injects the term into
the collectable on every `{:cont, term}` command.
The returned function receives a collectable and injects a given
value into it for every `{:cont, term}` instruction.
`:done` is passed as a command when no further values will be injected. This
is useful when there's a need to close resources or normalizing values. A
collectable must be returned when the command is `:done`.
`:done` is passed when no further values will be injected, useful
for closing resources and normalizing values. A collectable must
be returned on `:done`.
If injection is suddenly interrupted, `:halt` is passed and the function
can return any value as it won't be used.
For examples on how to use the `Collectable` protocol and `into/1` see the
module documentation.
If injection is suddenly interrupted, `:halt` is passed and it can
return any value, as it won't be used.
"""
@spec into(t) :: {term, (term, command -> t | term)}
def into(collectable)
+6 -5
View File
@@ -13,13 +13,14 @@ defmodule Dict do
@type value :: any
@type t :: list | map
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate every function by 1.4
defmacro __using__(_) do
# Use this import to guarantee proper code expansion
import Kernel, except: [size: 1]
%{file: file, line: line} = __CALLER__
:elixir_errors.warn(line, file, "the Dict module is deprecated")
quote do
def get(dict, key, default \\ nil) do
case fetch(dict, key) do
@@ -71,7 +72,7 @@ defmodule Dict do
end
def take(dict, keys) do
Enum.reduce(keys, new(), fn key, acc ->
Enum.reduce(keys, new, fn key, acc ->
case fetch(dict, key) do
{:ok, value} -> put(acc, key, value)
:error -> acc
@@ -165,7 +166,7 @@ defmodule Dict do
end
def split(dict, keys) do
Enum.reduce(keys, {new(), dict}, fn key, {inc, exc} = acc ->
Enum.reduce(keys, {new, dict}, fn key, {inc, exc} = acc ->
case fetch(exc, key) do
{:ok, value} ->
{put(inc, key, value), delete(exc, key)}
+535 -875
View File
File diff suppressed because it is too large Load Diff
+76 -342
View File
@@ -7,7 +7,7 @@ defmodule Exception do
`System.stacktrace/0` will return the stacktrace for the
last throw/error/exit that occurred in the current process.
Do not rely on the particular format returned by the `format*`
Do not rely on the particular format returned by the `format`
functions in this module. They may be changed in future releases
in order to better suit Elixir's tool chain. In other words,
by using the functions in this module it is guaranteed you will
@@ -22,8 +22,7 @@ defmodule Exception do
}
@typedoc "The kind handled by formatting functions"
@type kind :: :error | non_error_kind
@typep non_error_kind :: :exit | :throw | {:EXIT, pid}
@type kind :: :error | :exit | :throw | {:EXIT, pid}
@type stacktrace :: [stacktrace_entry]
@type stacktrace_entry ::
@@ -31,7 +30,7 @@ defmodule Exception do
{(... -> any), arity_or_args, location}
@typep arity_or_args :: non_neg_integer | list
@typep location :: keyword
@typep location :: Keyword.t
@callback exception(term) :: t
@callback message(t) :: String.t
@@ -81,7 +80,7 @@ defmodule Exception do
an empty stacktrace, `[]`, must be used.
"""
@spec normalize(:error, any, stacktrace) :: t
@spec normalize(non_error_kind, payload, stacktrace) :: payload when payload: var
@spec normalize(kind, payload, stacktrace) :: payload when payload: var
# Generating a stacktrace is expensive, default to nil
# to only fetch it when needed.
@@ -141,7 +140,9 @@ defmodule Exception do
Note that `{:EXIT, pid}` do not generate a stacktrace though
(as they are retrieved as messages without stacktraces).
"""
@spec format(kind, any, stacktrace | nil) :: String.t
def format(kind, payload, stacktrace \\ nil)
def format({:EXIT, _} = kind, any, _) do
@@ -157,162 +158,6 @@ defmodule Exception do
end
end
@doc """
Attaches information to exceptions for extra debugging.
This operation is potentially expensive, as it reads data
from the filesystem, parse beam files, evaluates code and
so on. Currently the following exceptions may be annotated:
* `FunctionClauseError` - annotated with the arguments
used on the call and available clauses
"""
@spec blame(:error, any, stacktrace) :: {t, stacktrace}
@spec blame(non_error_kind, payload, stacktrace) :: {payload, stacktrace} when payload: var
def blame(kind, error, stacktrace)
def blame(:error, error, stacktrace) do
case normalize(:error, error, stacktrace) do
%{__struct__: FunctionClauseError} = struct ->
blame_function_clause_error(struct, stacktrace)
_ ->
{error, stacktrace}
end
end
def blame(_kind, reason, stacktrace) do
{reason, stacktrace}
end
defp blame_function_clause_error(%{module: module, function: function, arity: arity} = exception,
[{module, function, args, meta} | rest])
when length(args) == arity do
exception =
case blame_mfa(module, function, args) do
{:ok, kind, clauses} -> %{exception | args: args, kind: kind, clauses: clauses}
:error -> %{exception | args: args}
end
{exception, [{module, function, arity, meta} | rest]}
end
defp blame_function_clause_error(exception, stacktrace) do
{exception, stacktrace}
end
@doc """
Blames the invocation of the given module, function and arguments.
This function will retrieve the available clauses from bytecode
and evaluate them against the given arguments. The clauses are
returned as a list of `{args, guards}` pairs where each argument
and each top-level condition in a guard separated by `and`/`or`
is wrapped in a tuple with blame metadata.
This function returns either `{:ok, definition, clauses}` or `:error`.
Where `definition` is `:def`, `:defp`, `:defmacro` or `:defmacrop`.
Note this functionality requires Erlang/OTP 20, otherwise `:error`
is always returned.
"""
@spec blame_mfa(module, function, args :: [term]) ::
{:ok, :def | :defp | :defmacro | :defmacrop, [{args :: [term], guards :: [term]}]} | :error
def blame_mfa(module, function, args) when is_atom(module) and is_atom(function) and is_list(args) do
try do
blame_mfa(module, function, length(args), args)
rescue
_ -> :error
end
end
defp blame_mfa(module, function, arity, call_args) do
with path when is_list(path) <- :code.which(module),
{:ok, {_, [debug_info: {:debug_info_v1, backend, data}]}} <- :beam_lib.chunks(path, [:debug_info]),
{:ok, %{definitions: defs}} <- backend.debug_info(:elixir_v1, module, data, []),
{_, kind, _, clauses} <- List.keyfind(defs, {function, arity}, 0) do
clauses =
for {meta, ex_args, guards, _block} <- clauses do
scope = :elixir_erl.definition_scope(meta, "nofile")
{erl_args, scope} =
:elixir_erl_clauses.match(&:elixir_erl_pass.translate_args/2, ex_args, scope)
{args, binding} =
[call_args, ex_args, erl_args]
|> Enum.zip()
|> Enum.map_reduce([], &blame_arg/2)
guards = Enum.map(guards, &blame_guard(&1, scope, binding))
{args, guards}
end
{:ok, kind, clauses}
else
_ -> :error
end
end
defp blame_arg({call_arg, ex_arg, erl_arg}, binding) do
{match?, binding} = blame_arg(erl_arg, call_arg, binding)
{blame_wrap(match?, rewrite_arg(ex_arg)), binding}
end
defp blame_arg(erl_arg, call_arg, binding) do
binding = :orddict.store(:VAR, call_arg, binding)
try do
{:value, _, binding} = :erl_eval.expr({:match, 0, erl_arg, {:var, 0, :VAR}}, binding, :none)
{true, binding}
rescue
_ -> {false, binding}
end
end
defp rewrite_arg(arg) do
Macro.prewalk(arg, fn
{:%{}, meta, [__struct__: Range, first: first, last: last]} ->
{:.., meta, [first, last]}
other ->
other
end)
end
defp blame_guard({{:., _, [:erlang, op]}, meta, [left, right]}, scope, binding)
when op == :andalso or op == :orelse do
{rewrite_guard_call(op), meta, [
blame_guard(left, scope, binding),
blame_guard(right, scope, binding)
]}
end
defp blame_guard(ex_guard, scope, binding) do
{erl_guard, _} = :elixir_erl_pass.translate(ex_guard, scope)
match? =
try do
{:value, true, _} = :erl_eval.expr(erl_guard, binding, :none)
true
rescue
_ -> false
end
blame_wrap(match?, rewrite_guard(ex_guard))
end
defp rewrite_guard(guard) do
Macro.prewalk(guard, fn
{:., _, [:erlang, call]} -> rewrite_guard_call(call)
other -> other
end)
end
defp rewrite_guard_call(:"orelse"), do: :or
defp rewrite_guard_call(:"andalso"), do: :and
defp rewrite_guard_call(:"=<"), do: :<=
defp rewrite_guard_call(:"/="), do: :!=
defp rewrite_guard_call(:"=:="), do: :===
defp rewrite_guard_call(:"=/="), do: :!==
defp rewrite_guard_call(op) when op in [:band, :bor, :bnot, :bsl, :bsr, :bxor],
do: {:., [], [Bitwise, op]}
defp rewrite_guard_call(op) when op in [:xor, :element, :size],
do: {:., [], [:erlang, op]}
defp rewrite_guard_call(op),
do: op
defp blame_wrap(match?, ast), do: %{match?: match?, node: ast}
@doc """
Formats an exit. It returns a string.
@@ -395,7 +240,7 @@ defmodule Exception do
defp format_exit_reason(:noconnection), do: "no connection"
defp format_exit_reason(:noproc) do
"no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started"
"no process"
end
defp format_exit_reason({:nodedown, node_name}) when is_atom(node_name) do
@@ -429,15 +274,15 @@ defmodule Exception do
end
defp format_exit_reason({:bad_start_spec, start_spec}) do
"bad child specification, invalid children: " <> inspect(start_spec)
"bad start spec: invalid children: " <> inspect(start_spec)
end
defp format_exit_reason({:start_spec, start_spec}) do
"bad child specification, " <> format_sup_spec(start_spec)
"bad start spec: " <> format_sup_spec(start_spec)
end
defp format_exit_reason({:supervisor_data, data}) do
"bad supervisor configuration, " <> format_sup_data(data)
"bad supervisor data: " <> format_sup_data(data)
end
defp format_exit_reason(reason), do: inspect(reason)
@@ -456,57 +301,44 @@ defmodule Exception do
end
defp format_sup_data({:invalid_intensity, intensity}) do
"invalid max_restarts (intensity): " <> inspect(intensity)
"invalid intensity: " <> inspect(intensity)
end
defp format_sup_data({:invalid_period, period}) do
"invalid max_seconds (period): " <> inspect(period)
"invalid period: " <> inspect(period)
end
defp format_sup_data(other), do: "got: #{inspect other}"
defp format_sup_spec({:duplicate_child_name, id}) do
"""
more than one child specification has the id: #{inspect id}.
If using maps as child specifications, make sure the :id keys are unique.
If using a module or {module, arg} as child, use Supervisor.child_spec/2 to change the :id, for example:
children = [
Supervisor.child_spec({MyWorker, arg}, id: :my_worker_1),
Supervisor.child_spec({MyWorker, arg}, id: :my_worker_2)
]
"""
end
defp format_sup_data(other), do: inspect(other)
defp format_sup_spec({:invalid_child_spec, child_spec}) do
"invalid child specification: #{inspect child_spec}"
"invalid child spec: " <> inspect(child_spec)
end
defp format_sup_spec({:invalid_child_type, type}) do
"invalid child type: #{inspect type}. Must be :worker or :supervisor."
"invalid child type: " <> inspect(type)
end
defp format_sup_spec({:invalid_mfa, mfa}) do
"invalid mfa: #{inspect mfa}"
"invalid mfa: " <> inspect(mfa)
end
defp format_sup_spec({:invalid_restart_type, restart}) do
"invalid restart type: #{inspect restart}. Must be :permanent, :transient or :temporary."
"invalid restart type: " <> inspect(restart)
end
defp format_sup_spec({:invalid_shutdown, shutdown}) do
"invalid shutdown: #{inspect shutdown}. Must be an integer >= 0, :infinity or :brutal_kill."
"invalid shutdown: " <> inspect(shutdown)
end
defp format_sup_spec({:invalid_module, mod}) do
"invalid module: #{inspect mod}. Must be an atom."
"invalid module: " <> inspect(mod)
end
defp format_sup_spec({:invalid_modules, modules}) do
"invalid modules: #{inspect modules}. Must be a list of atoms or :dynamic."
"invalid modules: " <> inspect(modules)
end
defp format_sup_spec(other), do: "got: #{inspect other}"
defp format_sup_spec(other), do: inspect(other)
@doc """
Receives a stacktrace entry and formats it into a string.
@@ -552,7 +384,7 @@ defmodule Exception do
is retrieved from `Process.info/2`.
"""
def format_stacktrace(trace \\ nil) do
trace = trace || case Process.info(self(), :current_stacktrace) do
trace = trace || case Process.info(self, :current_stacktrace) do
{:current_stacktrace, t} -> Enum.drop(t, 3)
end
@@ -597,12 +429,17 @@ defmodule Exception do
"anonymous fn in func/arity"
"""
def format_mfa(module, fun, arity) when is_atom(module) and is_atom(fun) do
case Inspect.Function.extract_anonymous_fun_parent(Atom.to_string(fun)) do
{outer_name, outer_arity} ->
"anonymous fn#{format_arity(arity)} in " <>
"#{inspect(module)}.#{Inspect.Function.escape_name(outer_name)}/#{outer_arity}"
:error ->
"#{inspect(module)}.#{Inspect.Function.escape_name(fun)}#{format_arity(arity)}"
fun =
case inspect(fun) do
":" <> fun -> fun
fun -> fun
end
case match?("\"-" <> _, fun) and String.split(fun, "-") do
[ "\"", outer_fun, "fun", _count, "\"" ] ->
"anonymous fn#{format_arity(arity)} in #{inspect module}.#{outer_fun}"
_ ->
"#{inspect module}.#{fun}#{format_arity(arity)}"
end
end
@@ -674,7 +511,7 @@ defmodule SystemLimitError do
end
defmodule SyntaxError do
defexception [:file, :line, description: "syntax error"]
defexception [file: nil, line: nil, description: "syntax error"]
def message(exception) do
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
@@ -683,7 +520,7 @@ defmodule SyntaxError do
end
defmodule TokenMissingError do
defexception [:file, :line, description: "expression is incomplete"]
defexception [file: nil, line: nil, description: "expression is incomplete"]
def message(%{file: file, line: line, description: description}) do
Exception.format_file_line(file && Path.relative_to_cwd(file), line) <>
@@ -692,7 +529,7 @@ defmodule TokenMissingError do
end
defmodule CompileError do
defexception [:file, :line, description: "compile error"]
defexception [file: nil, line: nil, description: "compile error"]
def message(%{file: file, line: line, description: description}) do
Exception.format_file_line(file && Path.relative_to_cwd(file), line) <>
@@ -701,7 +538,7 @@ defmodule CompileError do
end
defmodule BadFunctionError do
defexception [:term]
defexception [term: nil]
def message(exception) do
"expected a function, got: #{inspect(exception.term)}"
@@ -709,7 +546,7 @@ defmodule BadFunctionError do
end
defmodule BadStructError do
defexception [:struct, :term]
defexception [struct: nil, term: nil]
def message(exception) do
"expected a struct named #{inspect(exception.struct)}, got: #{inspect(exception.term)}"
@@ -717,23 +554,15 @@ defmodule BadStructError do
end
defmodule BadMapError do
defexception [:term]
defexception [term: nil]
def message(exception) do
"expected a map, got: #{inspect(exception.term)}"
end
end
defmodule BadBooleanError do
defexception [:term, :operator]
def message(exception) do
"expected a boolean on left-side of \"#{exception.operator}\", got: #{inspect(exception.term)}"
end
end
defmodule MatchError do
defexception [:term]
defexception [term: nil]
def message(exception) do
"no match of right hand side value: #{inspect(exception.term)}"
@@ -741,7 +570,7 @@ defmodule MatchError do
end
defmodule CaseClauseError do
defexception [:term]
defexception [term: nil]
def message(exception) do
"no case clause matching: #{inspect(exception.term)}"
@@ -749,7 +578,7 @@ defmodule CaseClauseError do
end
defmodule WithClauseError do
defexception [:term]
defexception [term: nil]
def message(exception) do
"no with clause matching: #{inspect(exception.term)}"
@@ -765,7 +594,7 @@ defmodule CondClauseError do
end
defmodule TryClauseError do
defexception [:term]
defexception [term: nil]
def message(exception) do
"no try clause matching: #{inspect(exception.term)}"
@@ -773,7 +602,7 @@ defmodule TryClauseError do
end
defmodule BadArityError do
defexception [:function, :args]
defexception [function: nil, args: nil]
def message(exception) do
fun = exception.function
@@ -789,7 +618,7 @@ defmodule BadArityError do
end
defmodule UndefinedFunctionError do
defexception [:module, :function, :arity, :reason, :exports]
defexception [module: nil, function: nil, arity: nil, reason: nil]
def message(%{reason: nil, module: module, function: function, arity: arity} = e) do
cond do
@@ -808,7 +637,8 @@ defmodule UndefinedFunctionError do
end
def message(%{reason: :"function not exported", module: module, function: function, arity: arity}) do
IO.iodata_to_binary(function_not_exported(module, function, arity, nil))
"function " <> Exception.format_mfa(module, function, arity) <>
" is undefined or private" <> did_you_mean(module, function, arity)
end
def message(%{reason: :"function not available", module: module, function: function, arity: arity}) do
@@ -821,24 +651,11 @@ defmodule UndefinedFunctionError do
"function " <> Exception.format_mfa(module, function, arity) <> " is undefined (#{reason})"
end
@doc false
def function_not_exported(module, function, arity, exports) do
suffix =
if macro_exported?(module, function, arity) do
". However there is a macro with the same name and arity. " <>
"Be sure to require #{inspect(module)} if you intend to invoke this macro"
else
did_you_mean(module, function, exports)
end
["function ", Exception.format_mfa(module, function, arity), " is undefined or private", suffix]
end
@function_threshold 0.77
@max_suggestions 5
defp did_you_mean(module, function, exports) do
exports = exports || exports_for(module)
defp did_you_mean(module, function, _arity) do
exports = exports_for(module)
result =
case Keyword.take(exports, [function]) do
@@ -856,13 +673,14 @@ defmodule UndefinedFunctionError do
|> Enum.sort(&elem(&1, 1) <= elem(&2, 1))
case result do
[] -> []
suggestions -> [". Did you mean one of:\n\n" | Enum.map(suggestions, &format_fa/1)]
[] -> ""
suggestions -> ". Did you mean one of:\n\n#{Enum.map(suggestions, &format_fa/1)}"
end
end
defp format_fa({_dist, fun, arity}) do
[" * ", Inspect.Function.escape_name(fun), ?/, Integer.to_string(arity), ?\n]
fun = with ":" <> fun <- inspect(fun), do: fun
" * " <> fun <> "/" <> Integer.to_string(arity) <> "\n"
end
defp exports_for(module) do
@@ -878,55 +696,16 @@ defmodule UndefinedFunctionError do
end
defmodule FunctionClauseError do
defexception [:module, :function, :arity, :kind, :args, :clauses]
defexception [module: nil, function: nil, arity: nil]
def message(exception) do
case exception do
%{function: nil} ->
"no function clause matches"
%{module: module, function: function, arity: arity} ->
formatted = Exception.format_mfa module, function, arity
"no function clause matching in #{formatted}" <> blame(exception, &inspect/1, &blame_match/2)
if exception.function do
formatted = Exception.format_mfa exception.module, exception.function, exception.arity
"no function clause matching in #{formatted}"
else
"no function clause matches"
end
end
defp blame_match(%{match?: true, node: node}, _),
do: Macro.to_string(node)
defp blame_match(%{match?: false, node: node}, _),
do: "-" <> Macro.to_string(node) <> "-"
defp blame_match(_, string),
do: string
@doc false
def blame(%{args: nil}, _, _) do
""
end
def blame(%{module: module, function: function, arity: arity,
kind: kind, args: args, clauses: clauses}, inspect_fun, ast_fun) do
mfa = Exception.format_mfa(module, function, arity)
formatted_args =
args
|> Enum.with_index(1)
|> Enum.map(fn {arg, i} -> "\n # #{i}\n #{inspect_fun.(arg)}\n" end)
formatted_clauses =
if clauses do
top_10 =
clauses
|> Enum.take(10)
|> Enum.map(fn {args, guards} ->
code = Enum.reduce(guards, {function, [], args}, &{:when, [], [&2, &1]})
" #{kind} " <> Macro.to_string(code, ast_fun) <> "\n"
end)
"\nAttempted function clauses (showing #{length(top_10)} out of #{length(clauses)}):\n\n#{top_10}"
else
""
end
"\n\nThe following arguments were given to #{mfa}:\n#{formatted_args}#{formatted_clauses}"
end
end
defmodule Code.LoadError do
@@ -939,30 +718,19 @@ defmodule Code.LoadError do
end
defmodule Protocol.UndefinedError do
defexception [:protocol, :value, description: ""]
defexception [protocol: nil, value: nil, description: ""]
def message(%{protocol: protocol, value: value, description: description}) do
"protocol #{inspect protocol} not implemented for #{inspect value}" <>
maybe_description(description) <> maybe_available(protocol)
end
defp maybe_description(""), do: ""
defp maybe_description(description), do: ", " <> description
defp maybe_available(protocol) do
case protocol.__protocol__(:impls) do
{:consolidated, []} ->
". There are no implementations for this protocol."
{:consolidated, types} ->
". This protocol is implemented for: #{Enum.map_join(types, ", ", &inspect/1)}"
:not_consolidated ->
""
def message(exception) do
msg = "protocol #{inspect exception.protocol} not implemented for #{inspect exception.value}"
case exception.description do
"" -> msg
descr -> msg <> ", " <> descr
end
end
end
defmodule KeyError do
defexception [:key, :term]
defexception key: nil, term: nil
def message(exception) do
msg = "key #{inspect exception.key} not found"
@@ -1006,23 +774,16 @@ defmodule Enum.EmptyError do
end
defmodule File.Error do
defexception [:reason, :path, action: ""]
defexception [reason: nil, action: "", path: nil]
def message(%{action: action, reason: reason, path: path}) do
formatted =
case {action, reason} do
{"remove directory", :eexist} ->
"directory is not empty"
_ ->
IO.iodata_to_binary(:file.format_error(reason))
end
"could not #{action} #{inspect(path)}: #{formatted}"
def message(exception) do
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
"could not #{exception.action} #{inspect(exception.path)}: #{formatted}"
end
end
defmodule File.CopyError do
defexception [:reason, :source, :destination, on: "", action: ""]
defexception [reason: nil, source: nil, destination: nil, on: "", action: ""]
def message(exception) do
formatted =
@@ -1039,22 +800,11 @@ defmodule File.CopyError do
end
end
defmodule File.LinkError do
defexception [:reason, :existing, :new, action: ""]
def message(exception) do
formatted =
IO.iodata_to_binary(:file.format_error(exception.reason))
"could not #{exception.action} from #{inspect(exception.existing)} to " <>
"#{inspect(exception.new)}: #{formatted}"
end
end
defmodule ErlangError do
defexception [:original]
defexception [original: nil]
def message(exception) do
"Erlang error: #{inspect(exception.original)}"
"erlang error: #{inspect(exception.original)}"
end
@doc false
@@ -1094,13 +844,9 @@ defmodule ErlangError do
%BadMapError{term: term}
end
def normalize({:badbool, op, term}, _stacktrace) do
%BadBooleanError{operator: op, term: term}
end
def normalize({:badkey, key}, stacktrace) do
term =
case ensure_stacktrace(stacktrace) do
case stacktrace || :erlang.get_stacktrace do
[{Map, :get_and_update!, [map, _, _], _} | _] -> map
[{Map, :update!, [map, _, _], _} | _] -> map
[{:maps, :update, [_, _, map], _} | _] -> map
@@ -1127,13 +873,13 @@ defmodule ErlangError do
end
def normalize(:undef, stacktrace) do
stacktrace = ensure_stacktrace(stacktrace)
stacktrace = stacktrace || :erlang.get_stacktrace
{mod, fun, arity} = from_stacktrace(stacktrace)
%UndefinedFunctionError{module: mod, function: fun, arity: arity}
end
def normalize(:function_clause, stacktrace) do
{mod, fun, arity} = from_stacktrace(ensure_stacktrace(stacktrace))
{mod, fun, arity} = from_stacktrace(stacktrace || :erlang.get_stacktrace)
%FunctionClauseError{module: mod, function: fun, arity: arity}
end
@@ -1145,18 +891,6 @@ defmodule ErlangError do
%ErlangError{original: other}
end
defp ensure_stacktrace(nil) do
try do
:erlang.get_stacktrace()
rescue
_ -> []
end
end
defp ensure_stacktrace(stacktrace) do
stacktrace
end
defp from_stacktrace([{module, function, args, _} | _]) when is_list(args) do
{module, function, length(args)}
end
+88 -241
View File
@@ -10,12 +10,6 @@ defmodule File do
via `cp/3` and remove files and directories recursively
via `rm_rf/1`.
Paths given to functions in this module can be either relative to the
current working directory (as returned by `File.cwd/0`), or absolute
paths. Shell conventions like `~` are not expanded automatically.
To use paths like `~/Downloads`, you can use `Path.expand/1` or
`Path.expand/2` to expand your path to an absolute path.
## Encoding
In order to write and read files, one must use the functions
@@ -76,6 +70,8 @@ defmodule File do
about such options and other performance considerations.
"""
alias :file, as: F
@type posix :: :file.posix()
@type io_device :: :file.io_device()
@type stat_options :: [time: :local | :universal | :posix]
@@ -89,9 +85,6 @@ defmodule File do
@doc """
Returns `true` if the path is a regular file.
This function follows symbolic links, so if a symbolic link points to a
regular file, `true` is returned.
## Examples
File.regular? __ENV__.file #=> true
@@ -103,28 +96,7 @@ defmodule File do
end
@doc """
Returns `true` if the given path is a directory.
This function follows symbolic links, so if a symbolic link points to a
directory, `true` is returned.
## Examples
File.dir?("./test")
#=> true
File.dir?("test")
#=> true
File.dir?("/usr/bin")
#=> true
File.dir?("~/Downloads")
#=> false
"~/Downloads" |> Path.expand |> File.dir?
#=> true
Returns `true` if the path is a directory.
"""
@spec dir?(Path.t) :: boolean
def dir?(path) do
@@ -150,7 +122,7 @@ defmodule File do
"""
@spec exists?(Path.t) :: boolean
def exists?(path) do
match?({:ok, _}, :file.read_file_info(IO.chardata_to_string(path)))
match?({:ok, _}, F.read_file_info(IO.chardata_to_string(path)))
end
@doc """
@@ -163,13 +135,13 @@ defmodule File do
directories of `path`
* `:eexist` - there is already a file or directory named `path`
* `:enoent` - a component of `path` does not exist
* `:enospc` - there is no space left on the device
* `:enospc` - there is a no space left on the device
* `:enotdir` - a component of `path` is not a directory;
on some platforms, `:enoent` is returned instead
"""
@spec mkdir(Path.t) :: :ok | {:error, posix}
def mkdir(path) do
:file.make_dir(IO.chardata_to_string(path))
F.make_dir(IO.chardata_to_string(path))
end
@doc """
@@ -193,7 +165,7 @@ defmodule File do
* `:eacces` - missing search or write permissions for the parent
directories of `path`
* `:enospc` - there is no space left on the device
* `:enospc` - there is a no space left on the device
* `:enotdir` - a component of `path` is not a directory
"""
@spec mkdir_p(Path.t) :: :ok | {:error, posix}
@@ -215,7 +187,7 @@ defmodule File do
{:error, :einval}
else
_ = do_mkdir_p(parent)
case :file.make_dir(path) do
case F.make_dir(path) do
{:error, :eexist} = error ->
if dir?(path), do: :ok, else: error
other ->
@@ -256,7 +228,7 @@ defmodule File do
"""
@spec read(Path.t) :: {:ok, binary} | {:error, posix}
def read(path) do
:file.read_file(IO.chardata_to_string(path))
F.read_file(IO.chardata_to_string(path))
end
@doc """
@@ -297,7 +269,7 @@ defmodule File do
@spec stat(Path.t, stat_options) :: {:ok, File.Stat.t} | {:error, posix}
def stat(path, opts \\ []) do
opts = Keyword.put_new(opts, :time, :universal)
case :file.read_file_info(IO.chardata_to_string(path), opts) do
case F.read_file_info(IO.chardata_to_string(path), opts) do
{:ok, fileinfo} ->
{:ok, File.Stat.from_record(fileinfo)}
error ->
@@ -306,7 +278,7 @@ defmodule File do
end
@doc """
Same as `stat/2` but returns the `File.Stat` directly, or
Same as `stat/2` but returns the `File.Stat` directly and
throws `File.Error` if an error is returned.
"""
@spec stat!(Path.t, stat_options) :: File.Stat.t | no_return
@@ -342,7 +314,7 @@ defmodule File do
@spec lstat(Path.t, stat_options) :: {:ok, File.Stat.t} | {:error, posix}
def lstat(path, opts \\ []) do
opts = Keyword.put_new(opts, :time, :universal)
case :file.read_link_info(IO.chardata_to_string(path), opts) do
case F.read_link_info(IO.chardata_to_string(path), opts) do
{:ok, fileinfo} ->
{:ok, File.Stat.from_record(fileinfo)}
error ->
@@ -351,7 +323,7 @@ defmodule File do
end
@doc """
Same as `lstat/2` but returns the `File.Stat` struct directly, or
Same as `lstat/2` but returns the `File.Stat` struct directly and
throws `File.Error` if an error is returned.
"""
@spec lstat!(Path.t, stat_options) :: File.Stat.t | no_return
@@ -364,44 +336,6 @@ defmodule File do
end
end
@doc """
Reads the symbolic link at `path`.
If `path` exists and is a symlink, returns `{:ok, target}`, otherwise returns
`{:error, reason}`.
For more details, see
[`:file.read_link/1`](http://erlang.org/doc/man/file.html#read_link-1).
Typical error reasons are:
* `:einval` - path is not a symbolic link
* `:enoent` - path does not exist
* `:enotsup` - symbolic links are not supported on the current platform
"""
@spec read_link(Path.t) :: {:ok, binary} | {:error, posix}
def read_link(path) do
case path |> IO.chardata_to_string |> :file.read_link do
{:ok, target} -> {:ok, IO.chardata_to_string(target)}
error -> error
end
end
@doc """
Same as `read_link/1` but returns the target directly or throws `File.Error` if an error is
returned.
"""
@spec read_link!(Path.t) :: binary | no_return
def read_link!(path) do
case read_link(path) do
{:ok, resolved} ->
resolved
{:error, reason} ->
raise File.Error, reason: reason, action: "read link", path: IO.chardata_to_string(path)
end
end
@doc """
Writes the given `File.Stat` back to the filesystem at the given
path. Returns `:ok` or `{:error, reason}`.
@@ -409,7 +343,7 @@ defmodule File do
@spec write_stat(Path.t, File.Stat.t, stat_options) :: :ok | {:error, posix}
def write_stat(path, stat, opts \\ []) do
opts = Keyword.put_new(opts, :time, :universal)
:file.write_file_info(IO.chardata_to_string(path), File.Stat.to_record(stat), opts)
F.write_file_info(IO.chardata_to_string(path), File.Stat.to_record(stat), opts)
end
@doc """
@@ -463,32 +397,6 @@ defmodule File do
end
end
@doc """
Creates a hard link `new` to the file `existing`.
Returns `:ok` if successful, `{:error, reason}` otherwise.
If the operating system does not support hard links, returns
`{:error, :enotsup}`.
"""
def ln(existing, new) do
:file.make_link(IO.chardata_to_string(existing), IO.chardata_to_string(new))
end
@doc """
Same as `ln/2` but raises an exception if it fails.
Returns `:ok` otherwise
"""
def ln!(existing, new) do
case ln(existing, new) do
:ok -> :ok
{:error, reason} ->
raise File.LinkError, reason: reason, action: "create hard link",
existing: IO.chardata_to_string(existing),
new: IO.chardata_to_string(new)
end
end
@doc """
Creates a symbolic link `new` to the file or directory `existing`.
@@ -497,22 +405,7 @@ defmodule File do
`{:error, :enotsup}`.
"""
def ln_s(existing, new) do
:file.make_symlink(IO.chardata_to_string(existing), IO.chardata_to_string(new))
end
@doc """
Same as `ln_s/2` but raises an exception if it fails.
Returns `:ok` otherwise
"""
def ln_s!(existing, new) do
case ln_s(existing, new) do
:ok -> :ok
{:error, reason} ->
raise File.LinkError, reason: reason, action: "create symlink",
existing: IO.chardata_to_string(existing),
new: IO.chardata_to_string(new)
end
F.make_symlink(existing, new)
end
@doc """
@@ -539,7 +432,7 @@ defmodule File do
"""
@spec copy(Path.t | io_device, Path.t | io_device, pos_integer | :infinity) :: {:ok, non_neg_integer} | {:error, posix}
def copy(source, destination, bytes_count \\ :infinity) do
:file.copy(maybe_to_string(source), maybe_to_string(destination), bytes_count)
F.copy(maybe_to_string(source), maybe_to_string(destination), bytes_count)
end
@doc """
@@ -563,7 +456,7 @@ defmodule File do
specify the `destination` filename, it is not sufficient to simply specify
its directory.
Returns `:ok` in case of success, `{:error, reason}` otherwise.
It returns `:ok` in case of success, returns `{:error, reason}` otherwise.
Note: The command `mv` in Unix systems behaves differently depending
if `source` is a file and the `destination` is an existing directory.
@@ -579,7 +472,7 @@ defmodule File do
"""
@spec rename(Path.t, Path.t) :: :ok | {:error, posix}
def rename(source, destination) do
:file.rename(source, destination)
F.rename(source, destination)
end
@doc """
@@ -640,9 +533,10 @@ defmodule File do
`destination`. If the source is a directory, it copies
the contents inside source into the destination.
If a file already exists in the destination, it invokes `callback`.
`callback` must be a function that takes two arguments: `source` and `destination`.
The callback should return `true` if the existing file should be overwritten and `false` otherwise.
If a file already exists in the destination,
it invokes a callback which should return
`true` if the existing file should be overwritten,
`false` otherwise. The callback defaults to return `true`.
If a directory already exists in the destination
where a file is meant to be (or vice versa), this
@@ -670,21 +564,15 @@ defmodule File do
File.cp_r "samples", "tmp"
# Same as before, but asks the user how to proceed in case of conflicts
File.cp_r "samples", "tmp", fn source, destination ->
File.cp_r "samples", "tmp", fn(source, destination) ->
IO.gets("Overwriting #{destination} by #{source}. Type y to confirm. ") == "y\n"
end
"""
@spec cp_r(Path.t, Path.t, (Path.t, Path.t -> boolean)) :: {:ok, [binary]} | {:error, posix, binary}
def cp_r(source, destination, callback \\ fn _, _ -> true end) when is_function(callback, 2) do
source =
source
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
destination =
destination
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.cp_r/3")
def cp_r(source, destination, callback \\ fn(_, _) -> true end) when is_function(callback) do
source = IO.chardata_to_string(source)
destination = IO.chardata_to_string(destination)
case do_cp_r(source, destination, callback, []) do
{:error, _, _} = error -> error
@@ -713,12 +601,12 @@ defmodule File do
{:ok, :regular} ->
do_cp_file(src, dest, callback, acc)
{:ok, :symlink} ->
case :file.read_link(src) do
case F.read_link(src) do
{:ok, link} -> do_cp_link(link, src, dest, callback, acc)
{:error, reason} -> {:error, reason, src}
end
{:ok, :directory} ->
case :file.list_dir(src) do
case F.list_dir(src) do
{:ok, files} ->
case mkdir(dest) do
success when success in [:ok, {:error, :eexist}] ->
@@ -746,7 +634,7 @@ defmodule File do
# Both src and dest are files.
defp do_cp_file(src, dest, callback, acc) do
case :file.copy(src, {dest, [:exclusive]}) do
case F.copy(src, {dest, [:exclusive]}) do
{:ok, _} ->
copy_file_mode!(src, dest)
[dest | acc]
@@ -767,14 +655,14 @@ defmodule File do
# Both src and dest are files.
defp do_cp_link(link, src, dest, callback, acc) do
case :file.make_symlink(link, dest) do
case F.make_symlink(link, dest) do
:ok ->
[dest | acc]
{:error, :eexist} ->
if path_differs?(src, dest) and callback.(src, dest) do
# If rm/1 fails, :file.make_symlink/2 will fail
# If rm/1 fails, F.make_symlink/2 will fail
_ = rm(dest)
case :file.make_symlink(link, dest) do
case F.make_symlink(link, dest) do
:ok -> [dest | acc]
{:error, reason} -> {:error, reason, src}
end
@@ -792,9 +680,6 @@ defmodule File do
contents are overwritten. Returns `:ok` if successful, or `{:error, reason}`
if an error occurs.
`content` must be `iodata` (a list of bytes or a binary). Setting the
encoding for this function has no effect.
**Warning:** Every time this function is invoked, a file descriptor is opened
and a new process is spawned to write to the file. For this reason, if you are
doing multiple writes in a loop, opening the file via `File.open/2` and using
@@ -806,7 +691,7 @@ defmodule File do
* `:enoent` - a component of the file name does not exist
* `:enotdir` - a component of the file name is not a directory;
on some platforms, `:enoent` is returned instead
* `:enospc` - there is no space left on the device
* `:enospc` - there is a no space left on the device
* `:eacces` - missing permission for writing the file or searching one of
the parent directories
* `:eisdir` - the named file is a directory
@@ -816,7 +701,7 @@ defmodule File do
@spec write(Path.t, iodata, [mode]) :: :ok | {:error, posix}
def write(path, content, modes \\ []) do
modes = normalize_modes(modes, false)
:file.write_file(IO.chardata_to_string(path), content, modes)
F.write_file(IO.chardata_to_string(path), content, modes)
end
@doc """
@@ -825,7 +710,7 @@ defmodule File do
@spec write!(Path.t, iodata, [mode]) :: :ok | no_return
def write!(path, content, modes \\ []) do
modes = normalize_modes(modes, false)
case :file.write_file(path, content, modes) do
case F.write_file(path, content, modes) do
:ok -> :ok
{:error, reason} ->
raise File.Error, reason: reason, action: "write to file",
@@ -861,7 +746,7 @@ defmodule File do
@spec rm(Path.t) :: :ok | {:error, posix}
def rm(path) do
path = IO.chardata_to_string(path)
case :file.delete(path) do
case F.delete(path) do
:ok ->
:ok
{:error, :eacces} = e ->
@@ -873,7 +758,7 @@ defmodule File do
defp change_mode_windows(path) do
if match? {:win32, _}, :os.type do
case :file.read_file_info(path) do
case F.read_file_info(path) do
{:ok, file_info} when elem(file_info, 3) in [:read, :none] ->
change_mode_windows(path, file_info)
_ ->
@@ -884,7 +769,7 @@ defmodule File do
defp change_mode_windows(path, file_info) do
case chmod(path, (elem(file_info, 7) + 0o200)) do
:ok -> :file.delete(path)
:ok -> F.delete(path)
{:error, _reason} = error -> error
end
end
@@ -917,7 +802,7 @@ defmodule File do
"""
@spec rmdir(Path.t) :: :ok | {:error, posix}
def rmdir(path) do
:file.del_dir(IO.chardata_to_string(path))
F.del_dir(IO.chardata_to_string(path))
end
@doc """
@@ -953,10 +838,7 @@ defmodule File do
"""
@spec rm_rf(Path.t) :: {:ok, [binary]} | {:error, posix, binary}
def rm_rf(path) do
path
|> IO.chardata_to_string()
|> assert_no_null_byte!("File.rm_rf/1")
|> do_rm_rf({:ok, []})
do_rm_rf(IO.chardata_to_string(path), {:ok, []})
end
defp do_rm_rf(path, {:ok, _} = entry) do
@@ -1017,7 +899,7 @@ defmodule File do
_ -> {:ok, :regular}
end
{:ok, :directory} ->
:file.list_dir(path)
F.list_dir(path)
{:ok, _} ->
{:ok, :regular}
{:error, reason} ->
@@ -1040,7 +922,7 @@ defmodule File do
end
@doc ~S"""
Opens the given `path`.
Opens the given `path` according to the given list of `modes`.
In order to write and read files, one must use the functions
in the `IO` module. By default, a file is opened in `:binary` mode,
@@ -1049,12 +931,6 @@ defmodule File do
option when opening the file and then all other functions from
`IO` are available, since they work directly with Unicode data.
`modes_or_function` can either be a list of modes or a function. If it's a
list, it's considered to be a list of modes (that are documented below). If
it's a function, then it's equivalent to calling `open(path, [],
modes_or_function)`. See the documentation for `open/3` for more information
on this function.
The allowed modes:
* `:binary` - opens the file in binary mode, disabling special handling of unicode sequences
@@ -1101,7 +977,7 @@ defmodule File do
* `{:ok, io_device}` - the file has been opened in the requested mode.
`io_device` is actually the PID of the process which handles the file.
`io_device` is actually the pid of the process which handles the file.
This process is linked to the process which originally opened the file.
If any process to which the `io_device` is linked terminates, the file
will be closed and the process itself will be terminated.
@@ -1120,13 +996,13 @@ defmodule File do
"""
@spec open(Path.t, [mode | :ram]) :: {:ok, io_device} | {:error, posix}
@spec open(Path.t, (io_device -> res)) :: {:ok, res} | {:error, posix} when res: var
def open(path, modes_or_function \\ [])
def open(path, modes \\ [])
def open(path, modes) when is_list(modes) do
:file.open(IO.chardata_to_string(path), normalize_modes(modes, true))
F.open(IO.chardata_to_string(path), normalize_modes(modes, true))
end
def open(path, function) when is_function(function, 1) do
def open(path, function) when is_function(function) do
open(path, [], function)
end
@@ -1137,7 +1013,7 @@ defmodule File do
automatically closed after the function returns, regardless
if there was an error when executing the function.
Returns `{:ok, function_result}` in case of success,
It returns `{:ok, function_result}` in case of success,
`{:error, reason}` otherwise.
This function expects the file to be closed with success,
@@ -1151,51 +1027,43 @@ defmodule File do
IO.read(file, :line)
end)
See `open/2` for the list of available `modes`.
"""
@spec open(Path.t, [mode | :ram], (io_device -> res)) :: {:ok, res} | {:error, posix} when res: var
def open(path, modes, function) when is_list(modes) and is_function(function, 1) do
def open(path, modes, function) do
case open(path, modes) do
{:ok, io_device} ->
{:ok, device} ->
try do
{:ok, function.(io_device)}
{:ok, function.(device)}
after
:ok = close(io_device)
:ok = close(device)
end
other -> other
end
end
@doc """
Similar to `open/2` but raises an error if file could not be opened.
Same as `open/2` but raises an error if file could not be opened.
Returns the IO device otherwise.
See `open/2` for the list of available modes.
Returns the `io_device` otherwise.
"""
@spec open!(Path.t, [mode | :ram]) :: io_device | no_return
@spec open!(Path.t, (io_device -> res)) :: res | no_return when res: var
def open!(path, modes_or_function \\ []) do
case open(path, modes_or_function) do
{:ok, io_device_or_function_result} ->
io_device_or_function_result
@spec open!(Path.t, [mode]) :: io_device | no_return
def open!(path, modes \\ []) do
case open(path, modes) do
{:ok, device} -> device
{:error, reason} ->
raise File.Error, reason: reason, action: "open", path: IO.chardata_to_string(path)
end
end
@doc """
Similar to `open/3` but raises an error if file could not be opened.
Same as `open/3` but raises an error if file could not be opened.
If it succeeds opening the file, it returns the `function` result on the IO device.
See `open/2` for the list of available `modes`.
Returns the function result otherwise.
"""
@spec open!(Path.t, [mode | :ram], (io_device -> res)) :: res | no_return when res: var
def open!(path, modes, function) do
case open(path, modes, function) do
{:ok, function_result} ->
function_result
{:ok, device} -> device
{:error, reason} ->
raise File.Error, reason: reason, action: "open", path: IO.chardata_to_string(path)
end
@@ -1211,7 +1079,7 @@ defmodule File do
"""
@spec cwd() :: {:ok, binary} | {:error, posix}
def cwd() do
case :file.get_cwd do
case F.get_cwd do
{:ok, base} -> {:ok, IO.chardata_to_string(fix_drive_letter(base))}
{:error, _} = error -> error
end
@@ -1219,7 +1087,7 @@ defmodule File do
defp fix_drive_letter([l, ?:, ?/ | rest] = original) when l in ?A..?Z do
case :os.type() do
{:win32, _} -> [l + ?a - ?A, ?:, ?/ | rest]
{:win32, _} -> [l+?a-?A, ?:, ?/ | rest]
_ -> original
end
end
@@ -1245,7 +1113,7 @@ defmodule File do
"""
@spec cd(Path.t) :: :ok | {:error, posix}
def cd(path) do
:file.set_cwd(IO.chardata_to_string(path))
F.set_cwd(IO.chardata_to_string(path))
end
@doc """
@@ -1269,9 +1137,9 @@ defmodule File do
Raises an error if retrieving or changing the current
directory fails.
"""
@spec cd!(Path.t, (() -> res)) :: res when res: var
@spec cd!(Path.t, (() -> res)) :: res | no_return when res: var
def cd!(path, function) do
old = cwd!()
old = cwd!
cd!(path)
try do
function.()
@@ -1283,12 +1151,12 @@ defmodule File do
@doc """
Returns the list of files in the given directory.
Returns `{:ok, [files]}` in case of success,
It returns `{:ok, [files]}` in case of success,
`{:error, reason}` otherwise.
"""
@spec ls(Path.t) :: {:ok, [binary]} | {:error, posix}
def ls(path \\ ".") do
case :file.list_dir(IO.chardata_to_string(path)) do
case F.list_dir(IO.chardata_to_string(path)) do
{:ok, file_list} -> {:ok, Enum.map(file_list, &IO.chardata_to_string/1)}
{:error, _} = error -> error
end
@@ -1314,11 +1182,11 @@ defmodule File do
Note that if the option `:delayed_write` was used when opening the file,
`close/1` might return an old write error and not even try to close the file.
See `open/2` for more information.
See `open/2`.
"""
@spec close(io_device) :: :ok | {:error, posix | :badarg | :terminated}
def close(io_device) do
:file.close(io_device)
F.close(io_device)
end
@doc """
@@ -1327,7 +1195,7 @@ defmodule File do
The stream implements both `Enumerable` and `Collectable` protocols,
which means it can be used both for read and write.
The `line_or_bytes` argument configures how the file is read when
The `line_or_byte` argument configures how the file is read when
streaming, by `:line` (default) or by a given number of bytes.
Operating the stream can fail on open for the same reasons as
@@ -1342,23 +1210,18 @@ defmodule File do
in raw mode for performance reasons. Therefore, Elixir **will** open
streams in `:raw` mode with the `:read_ahead` option unless an encoding
is specified. This means any data streamed into the file must be
converted to `t:iodata/0` type. If you pass `[:utf8]` in the modes parameter,
converted to `iodata` type. If you pass `[:utf8]` in the modes parameter,
the underlying stream will use `IO.write/2` and the `String.Chars` protocol
to convert the data. See `IO.binwrite/2` and `IO.write/2` .
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
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.
## Examples
# 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],
#=> %File.Stream{line_or_bytes: 2048, modes: [:raw, :read_ahead, :binary],
#=> path: "./test/test.data", raw: true}
See `Stream.run/1` for an example of streaming into a file.
@@ -1376,28 +1239,26 @@ defmodule File do
## Permissions
File permissions are specified by adding together the following octal flags:
* 0o400 - read permission: owner
* 0o200 - write permission: owner
* 0o100 - execute permission: owner
* `0o400` - read permission: owner
* `0o200` - write permission: owner
* `0o100` - execute permission: owner
* 0o040 - read permission: group
* 0o020 - write permission: group
* 0o010 - execute permission: group
* `0o040` - read permission: group
* `0o020` - write permission: group
* `0o010` - execute permission: group
* 0o004 - read permission: other
* 0o002 - write permission: other
* 0o001 - execute permission: other
* `0o004` - read permission: other
* `0o002` - write permission: other
* `0o001` - execute permission: other
For example, setting the mode `0o755` gives it
For example, setting the mode 0o755 gives it
write, read and execute permission to the owner
and both read and execute permission to group
and others.
"""
@spec chmod(Path.t, non_neg_integer) :: :ok | {:error, posix}
def chmod(path, mode) do
:file.change_mode(IO.chardata_to_string(path), mode)
F.change_mode(IO.chardata_to_string(path), mode)
end
@doc """
@@ -1420,7 +1281,7 @@ defmodule File do
"""
@spec chgrp(Path.t, non_neg_integer) :: :ok | {:error, posix}
def chgrp(path, gid) do
:file.change_group(IO.chardata_to_string(path), gid)
F.change_group(IO.chardata_to_string(path), gid)
end
@doc """
@@ -1443,7 +1304,7 @@ defmodule File do
"""
@spec chown(Path.t, non_neg_integer) :: :ok | {:error, posix}
def chown(path, uid) do
:file.change_owner(IO.chardata_to_string(path), uid)
F.change_owner(IO.chardata_to_string(path), uid)
end
@doc """
@@ -1463,26 +1324,14 @@ defmodule File do
@read_ahead_size 64 * 1024
defp assert_no_null_byte!(binary, operation) do
case :binary.match(binary, "\0") do
{_, _} ->
raise ArgumentError, "cannot execute #{operation} for path with null byte, got: #{inspect binary}"
:nomatch ->
binary
end
end
defp normalize_modes([:utf8 | rest], binary?) do
[encoding: :utf8] ++ normalize_modes(rest, binary?)
end
defp normalize_modes([:read_ahead | rest], binary?) do
[read_ahead: @read_ahead_size] ++ normalize_modes(rest, binary?)
end
# TODO: Remove :char_list mode by 2.0
# TODO: Deprecate :char_list mode by v1.5
defp normalize_modes([mode | rest], _binary?) when mode in [:charlist, :char_list] do
if mode == :char_list do
IO.warn "the :char_list mode is deprecated, use :charlist"
end
normalize_modes(rest, false)
end
defp normalize_modes([mode | rest], binary?) do
@@ -1491,10 +1340,8 @@ defmodule File do
defp normalize_modes([], true), do: [:binary]
defp normalize_modes([], false), do: []
defp maybe_to_string(path) when is_list(path),
do: IO.chardata_to_string(path)
defp maybe_to_string(path) when is_binary(path),
defp maybe_to_string(path) when is_pid(path),
do: path
defp maybe_to_string(path),
do: path
do: IO.chardata_to_string(path)
end
+7 -66
View File
@@ -36,7 +36,7 @@ defmodule File.Stream do
defimpl Collectable do
def into(%{path: path, modes: modes, raw: raw} = stream) do
modes = for mode <- modes, mode not in [:read], do: mode
modes = for mode <- modes, not mode in [:read], do: mode
case :file.open(path, [:write | modes]) do
{:ok, device} ->
@@ -67,14 +67,13 @@ defmodule File.Stream do
end
defimpl Enumerable do
@read_ahead_size 64 * 1024
def reduce(%{path: path, modes: modes, line_or_bytes: line_or_bytes, raw: raw}, acc, fun) do
modes = for mode <- modes, not mode in [:write, :append], do: mode
start_fun =
fn ->
case :file.open(path, read_modes(modes)) do
{:ok, device} ->
if :trim_bom in modes, do: trim_bom(device), else: device
case :file.open(path, modes) do
{:ok, device} -> device
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
@@ -89,70 +88,12 @@ defmodule File.Stream do
Stream.resource(start_fun, next_fun, &:file.close/1).(acc, fun)
end
def count(%{path: path, modes: modes, line_or_bytes: :line} = stream) do
pattern = :binary.compile_pattern("\n")
counter = &count_lines(&1, path, pattern, read_function(stream), 0)
case File.open(path, read_modes(modes), counter) do
{:ok, count} ->
{:ok, count}
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
end
def count(%{path: path, line_or_bytes: bytes}) do
case File.stat(path) do
{:ok, %{size: 0}} ->
{:error, __MODULE__}
{:ok, %{size: size}} ->
{:ok, div(size, bytes) + if(rem(size, bytes) == 0, do: 0, else: 1)}
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
def count(_stream) do
{:error, __MODULE__}
end
def member?(_stream, _term) do
{:error, __MODULE__}
end
defp trim_bom(device) do
header = IO.binread(device, 4)
{:ok, _new_pos} = :file.position(device, bom_length(header))
device
end
defp bom_length(<<239, 187, 191, _rest::binary>>),
do: 3
defp bom_length(<<254, 255, _rest::binary>>),
do: 2
defp bom_length(<<255, 254, _rest::binary>>),
do: 2
defp bom_length(<<0, 0, 254, 255, _rest::binary>>),
do: 4
defp bom_length(<<254, 255, 0, 0, _rest::binary>>),
do: 4
defp bom_length(_binary),
do: 0
defp read_modes(modes) do
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
end
defp count_lines(device, path, pattern, read, count) do
case read.(device) do
data when is_binary(data) ->
count_lines(device, path, pattern, read, count + count_lines(data, pattern))
:eof ->
count
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
end
end
defp count_lines(data, pattern), do: length(:binary.matches(data, pattern))
defp read_function(%{raw: true}), do: &IO.binread(&1, @read_ahead_size)
defp read_function(%{raw: false}), do: &IO.read(&1, @read_ahead_size)
end
end
+42 -257
View File
@@ -2,15 +2,9 @@ import Kernel, except: [round: 1]
defmodule Float do
@moduledoc """
Functions for working with floating-point numbers.
Functions for working with floating point numbers.
"""
import Bitwise
@power_of_2_to_52 4503599627370496
@precision_range 0..15
@type precision_range :: 0..15
@doc """
Parses a binary into a float.
@@ -28,8 +22,10 @@ defmodule Float do
iex> Float.parse("34")
{34.0, ""}
iex> Float.parse("34.25")
{34.25, ""}
iex> Float.parse("56.5xyz")
{56.5, "xyz"}
@@ -80,20 +76,8 @@ defmodule Float do
@doc """
Rounds a float to the largest integer less than or equal to `num`.
`floor/2` also accepts a precision to round a floating-point value down
`floor/2` also accepts a precision to round a floating point value down
to an arbitrary number of fractional digits (between 0 and 15).
The operation is performed on the binary floating point, without a
conversion to decimal.
The behaviour of `floor/2` for floats can be surprising. For example:
iex> Float.floor(12.52, 2)
12.51
One may have expected it to floor to 12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 12.51999999,
which explains the behaviour above.
This function always returns a float. `Kernel.trunc/1` may be used instead to
truncate the result to an integer afterwards.
@@ -102,42 +86,29 @@ defmodule Float do
iex> Float.floor(34.25)
34.0
iex> Float.floor(-56.5)
-57.0
iex> Float.floor(34.259, 2)
34.25
"""
@spec floor(float, precision_range) :: float
def floor(number, precision \\ 0)
def floor(number, precision) when is_float(number) and precision in @precision_range do
round(number, precision, :floor)
end
def floor(number, precision) when is_float(number) do
raise ArgumentError, invalid_precision_message(precision)
@spec floor(float, 0..15) :: float
def floor(number, precision \\ 0) when is_float(number) and precision in 0..15 do
power = power_of_10(precision)
number = number * power
truncated = trunc(number)
variance = if number - truncated < 0, do: -1.0, else: 0.0
(truncated + variance) / power
end
@doc """
Rounds a float to the smallest integer greater than or equal to `num`.
`ceil/2` also accepts a precision to round a floating-point value down
`ceil/2` also accepts a precision to round a floating point value down
to an arbitrary number of fractional digits (between 0 and 15).
The operation is performed on the binary floating point, without a
conversion to decimal.
The behaviour of `ceil/2` for floats can be surprising. For example:
iex> Float.ceil(-12.52, 2)
-12.51
One may have expected it to ceil to -12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as -12.51999999,
which explains the behaviour above.
This function always returns floats. `Kernel.trunc/1` may be used instead to
truncate the result to an integer afterwards.
@@ -145,239 +116,60 @@ defmodule Float do
iex> Float.ceil(34.25)
35.0
iex> Float.ceil(-56.5)
-56.0
iex> Float.ceil(34.251, 2)
34.26
"""
@spec ceil(float, precision_range) :: float
def ceil(number, precision \\ 0)
def ceil(number, precision) when is_float(number) and precision in @precision_range do
round(number, precision, :ceil)
end
def ceil(number, precision) when is_float(number) do
raise ArgumentError, invalid_precision_message(precision)
@spec ceil(float, 0..15) :: float
def ceil(number, precision \\ 0) when is_float(number) and precision in 0..15 do
power = power_of_10(precision)
number = number * power
truncated = trunc(number)
variance = if number - truncated > 0, do: 1.0, else: 0.0
(truncated + variance) / power
end
@doc """
Rounds a floating-point value to an arbitrary number of fractional
digits (between 0 and 15).
The rounding direction always ties to half up. The operation is
performed on the binary floating point, without a conversion to decimal.
Rounds a floating point value to an arbitrary number of fractional digits
(between 0 and 15).
This function only accepts floats and always returns a float. Use
`Kernel.round/1` if you want a function that accepts both floats
and integers and always returns an integer.
The behaviour of `round/2` for floats can be surprising. For example:
iex> Float.round(5.5675, 3)
5.567
One may have expected it to round to the half up 5.568. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 5.567499999,
which explains the behaviour above. If you want exact rounding for decimals,
you must use a decimal library. The behaviour above is also in accordance
to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay.
`Kernel.round/1` if you want a function that accepts both floats and integers
and always returns an integer.
## Examples
iex> Float.round(12.5)
13.0
iex> Float.round(5.5674, 3)
5.567
iex> Float.round(5.5675, 3)
5.567
5.568
iex> Float.round(-5.5674, 3)
-5.567
iex> Float.round(-5.5675, 3)
-5.568
iex> Float.round(-5.5675)
-6.0
iex> Float.round(12.341444444444441, 15)
12.341444444444441
"""
@spec round(float, precision_range) :: float
# This implementation is slow since it relies on big integers.
# Faster implementations are available on more recent papers
# and could be implemented in the future.
def round(float, precision \\ 0)
def round(float, precision) when is_float(float) and precision in @precision_range do
round(float, precision, :half_up)
@spec round(float, 0..15) :: float
def round(number, precision \\ 0) when is_float(number) and precision in 0..15 do
power = power_of_10(precision)
Kernel.round(number * power) / power
end
def round(number, precision) when is_float(number) do
raise ArgumentError, invalid_precision_message(precision)
end
defp round(float, precision, rounding) do
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, count, _} = decompose(significant)
count = count - exp + 1023
cond do
count <= 0 or # There is no decimal precision
(0 == exp and <<0::52>> == significant) -> #zero or minus zero
float
count >= 104 -> # Precision beyond 15 digits
case rounding do
:ceil when sign === 0 -> 1 / power_of_10(precision)
:floor when sign === 1 -> -1 / power_of_10(precision)
_ -> 0.0
end
count <= precision -> # We are asking more precision than we have
float
true ->
# Difference in precision between float and asked precision
# We subtract 1 because we need to calculate the remainder too
diff = count - precision - 1
# Get up to latest so we calculate the remainder
power_of_10 = power_of_10(diff)
# Convert the numerand to decimal base
num = num * power_of_5(count)
# Move to the given precision - 1
num = div(num, power_of_10)
div = div(num, 10)
num = rounding(rounding, sign, num, div)
# Convert back to float without loss
# http://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/
den = power_of_10(precision)
boundary = den <<< 52
cond do
num == 0 ->
0.0
num >= boundary ->
{den, exp} = scale_down(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
true ->
{num, exp} = scale_up(num, boundary, 52)
decimal_to_float(sign, num, den, exp)
end
end
end
defp scale_up(num, boundary, exp) when num >= boundary, do: {num, exp}
defp scale_up(num, boundary, exp), do: scale_up(num <<< 1, boundary, exp - 1)
defp scale_down(num, den, exp) do
new_den = den <<< 1
if num < new_den do
{den >>> 52, exp}
else
scale_down(num, new_den, exp + 1)
end
end
defp decimal_to_float(sign, num, den, exp) do
quo = div(num, den)
rem = num - quo * den
tmp =
case den >>> 1 do
den when rem > den -> quo + 1
den when rem < den -> quo
_ when (quo &&& 1) === 1 -> quo + 1
_ -> quo
end
tmp = tmp - @power_of_2_to_52
<<tmp::float>> = <<sign::1, (exp + 1023)::11, tmp::52>>
tmp
end
defp rounding(:floor, 1, _num, div), do: div + 1
defp rounding(:ceil, 0, _num, div), do: div + 1
defp rounding(:half_up, _sign, num, div) do
case rem(num, 10) do
rem when rem < 5 -> div
rem when rem >= 5 -> div + 1
end
end
defp rounding(_, _, _, div), do: div
Enum.reduce 0..104, 1, fn x, acc ->
Enum.reduce 0..15, 1, fn x, acc ->
defp power_of_10(unquote(x)), do: unquote(acc)
acc * 10
end
Enum.reduce 0..104, 1, fn x, acc ->
defp power_of_5(unquote(x)), do: unquote(acc)
acc * 5
end
@doc """
Returns a pair of integers whose ratio is exactly equal
to the original float and with a positive denominator.
## Examples
iex> Float.ratio(3.14)
{7070651414971679, 2251799813685248}
iex> Float.ratio(-3.14)
{-7070651414971679, 2251799813685248}
iex> Float.ratio(1.5)
{3, 2}
iex> Float.ratio(-1.5)
{-3, 2}
iex> Float.ratio(16.0)
{16, 1}
iex> Float.ratio(-16.0)
{-16, 1}
"""
def ratio(float) when is_float(float) do
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
{num, _, den} = decompose(significant)
num = sign(sign, num)
case exp - 1023 do
exp when exp > 0 ->
{den, exp} = shift_right(den, exp)
{shift_left(num, exp), den}
exp when exp < 0 ->
{num, shift_left(den, -exp)}
0 ->
{num, den}
end
end
defp decompose(significant) do
decompose(significant, 1, 0, 2, 1, 1)
end
defp decompose(<<1::1, bits::bitstring>>, count, last_count, power, _last_power, acc) do
decompose(bits, count + 1, count, power <<< 1, power, shift_left(acc, count - last_count) + 1)
end
defp decompose(<<0::1, bits::bitstring>>, count, last_count, power, last_power, acc) do
decompose(bits, count + 1, last_count, power <<< 1, last_power, acc)
end
defp decompose(<<>>, _count, last_count, _power, last_power, acc) do
{acc, last_count, last_power}
end
defp sign(0, num), do: num
defp sign(1, num), do: -num
defp shift_left(num, 0), do: num
defp shift_left(num, times), do: shift_left(num <<< 1, times - 1)
defp shift_right(num, 0), do: {num, 0}
defp shift_right(1, times), do: {1, times}
defp shift_right(num, times), do: shift_right(num >>> 1, times - 1)
@doc """
Returns a charlist which corresponds to the text representation
of the given float.
@@ -418,29 +210,22 @@ defmodule Float do
IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
end
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by v1.5
@doc false
def to_char_list(float), do: Float.to_charlist(float)
# TODO: Deprecate by v1.4
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
def to_char_list(float, options) do
:erlang.float_to_list(float, expand_compact(options))
end
# TODO: Deprecate by v1.4
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
def to_string(float, options) do
:erlang.float_to_binary(float, expand_compact(options))
end
defp invalid_precision_message(precision) do
"precision #{precision} is out of valid range of #{inspect @precision_range}"
end
defp expand_compact([{:compact, false} | t]), do: expand_compact(t)
defp expand_compact([{:compact, true} | t]), do: [:compact | expand_compact(t)]
defp expand_compact([h | t]), do: [h | expand_compact(t)]
+437 -83
View File
@@ -1,90 +1,302 @@
defmodule GenEvent do
# TODO: Remove by 2.0
# Functions from this module are deprecated in elixir_dispatch.
@moduledoc """
WARNING: this module is deprecated.
A behaviour module for implementing event handling functionality.
If you are interested in implementing an event manager, please read the
"Alternatives" section below. If you have to implement an event handler to
integrate with an existing system, such as Elixir's Logger, please use
`:gen_event` instead.
The event handling model consists of a generic event manager
process with an arbitrary number of event handlers which are
added and deleted dynamically.
## Alternatives
An event manager implemented using this module will have a standard
set of interface functions and include functionality for tracing and
error reporting. It will also fit into a supervision tree.
There are a few suitable alternatives to replace GenEvent. Each of them can be
the most beneficial based on the use case.
## Example
### Supervisor and GenServers
There are many use cases for event handlers. For example, a logging
system can be built using event handlers where each log message is
an event and different event handlers can be attached to handle the
log messages. One handler may print error messages on the terminal,
another can write it to a file, while a third one can keep the
messages in memory (like a buffer) until they are read.
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)
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/)
has more detailed information on this approach.
As an example, let's have a GenEvent that accumulates messages until
they are collected by an explicit call.
### GenStage
# Define an Event Handler
defmodule LoggerHandler do
use GenEvent
If the use case where you were using GenEvent requires more complex logic,
[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 tool to implement systems that exchange events in a
demand-driven way with built-in support for backpressure. See the [GenStage
documentation](https://hexdocs.pm/gen_stage) for more information.
# Callbacks
### `:gen_event`
def handle_event({:log, x}, messages) do
{:ok, [x | messages]}
end
If your use case requires exactly what GenEvent provided, or you have to
integrate with an existing `:gen_event`-based system, you can still use the
[`:gen_event`](http://erlang.org/doc/man/gen_event.html) Erlang module.
def handle_call(:messages, messages) do
{:ok, Enum.reverse(messages), []}
end
end
# Start a new event manager.
{:ok, pid} = GenEvent.start_link([])
# Attach an event handler to the event manager.
GenEvent.add_handler(pid, LoggerHandler, [])
#=> :ok
# Send some events to the event manager.
GenEvent.notify(pid, {:log, 1})
#=> :ok
GenEvent.notify(pid, {:log, 2})
#=> :ok
# Call functions on specific handlers in the manager.
GenEvent.call(pid, LoggerHandler, :messages)
#=> [1, 2]
GenEvent.call(pid, LoggerHandler, :messages)
#=> []
We start a new event manager by calling `GenEvent.start_link/1`.
Notifications can be sent to the event manager which will then
invoke `handle_event/2` for each registered handler.
We can add new handlers with `add_handler/3` and `add_mon_handler/3`.
Calls can also be made to specific handlers by using `call/3`.
## Callbacks
There are 6 callbacks required to be implemented in a `GenEvent`. By
adding `use GenEvent` to your module, Elixir will automatically define
all 6 callbacks for you, leaving it up to you to implement the ones
you want to customize.
## Name Registration
A GenEvent is bound to the same name registration rules as a `GenServer`.
Read more about it in the `GenServer` docs.
## Modes
GenEvent supports three different notifications.
On `GenEvent.ack_notify/2`, the manager acknowledges each event,
providing backpressure, but processing of the message happens
asynchronously.
On `GenEvent.sync_notify/2`, the manager acknowledges an event
just after it is processed by all event handlers.
On `GenEvent.notify/2`, all events are processed asynchronously and
there is no ack (which means there is no backpressure).
## Streaming
`GenEvent` messages can be streamed with the help of `stream/2`.
You will need to start another process to consume the stream:
Task.start_link fn ->
stream = GenEvent.stream(pid)
# Discard the next 3 events
_ = Enum.take(stream, 3)
# Print all remaining events
for event <- stream do
IO.inspect event
end
end
Now call `GenEvent.notify/2` multiple times. You will see the
first three events will be skipped while the rest will be
continuously printed.
## Learn more and compatibility
If you wish to find out more about GenEvent, the documentation and links
in Erlang can provide extra insight.
* [`:gen_event` module documentation](http://www.erlang.org/doc/man/gen_event.html)
* [Event Handlers – Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/event-handlers)
Keep in mind though Elixir and Erlang gen events are not 100% compatible.
The `:gen_event.add_sup_handler/3` is not supported by Elixir's GenEvent,
which in turn supports `GenEvent.add_mon_handler/3`.
The benefits of the monitoring approach are described in the "Don't drink
too much kool aid" section of the "Learn you some Erlang" link above. Due
to those changes, Elixir's GenEvent does not trap exits by default.
Furthermore, Elixir also normalizes the `{:error, _}` tuples returned
by many functions, in order to be more consistent with themselves and
the `GenServer` module.
"""
@doc """
Invoked when the handler is added to the `GenEvent` process. `add_handler/3`
(and `add_mon_handler/3`) will block until it returns.
`args` is the argument term (third argument) passed to `add_handler/3`.
Returning `{:ok, state}` will cause `add_handler/3` to return `:ok` and the
handler to become part of the `GenEvent` loop with state `state`.
Returning `{:ok, state, :hibernate}` is similar to
`{:ok, state}` except the `GenEvent` process is hibernated before continuing
its loop. See `handle_event/2` for more information on hibernation.
Returning `{:error, reason}` will cause `add_handler/3` to return
`{:error, reason}` and the handler is not added to `GenEvent` loop.
"""
@callback init(args :: term) ::
{:ok, state} |
{:ok, state, :hibernate} |
{:error, reason :: any} when state: any
@doc """
Invoked to handle `notify/2`, `ack_notify/2` or `sync_notify/2` messages.
`event` is the event message and `state` is the current state of the handler.
Returning `{:ok, new_state}` sets the handler's state to `new_state` and the
`GenEvent` loop continues.
Returning `{:ok, new_state, :hibernate}` is similar to
`{:ok, new_state}` except the process is hibernated once all handlers have
handled the events. The `GenEvent` process will continue the loop once a
message is its message queue. If a message is already in the message queue
this will be immediately. Hibernating a `GenEvent` causes garbage collection
and leaves a continuous heap that minimises the memory used by the process.
Hibernating should not be used aggressively as too much time could be spent
garbage collecting. Normally it should only be used when a message is not
expected soon and minimising the memory of the process is shown to be
beneficial.
Returning `:remove_handler` removes the handler from the `GenEvent` loop and
calls `terminate/2` with reason `:remove_handler` and state `state`.
"""
@callback handle_event(event :: term, state :: term) ::
{:ok, new_state} |
{:ok, new_state, :hibernate} |
:remove_handler when new_state: term
@doc """
Invoked to handle synchronous `call/4` messages to a specific handler.
`request` is the request message sent by a `call/4` and `state` is the current
state of the handler.
Returning `{:ok, reply, new_state}` sends `reply` as a response to the call
and sets the handler's state to `new_state`.
Returning `{:ok, reply, new_state, :hibernate}` is similar to
`{:ok, reply, new_state}` except the process is hibernated. See
`handle_event/2` for more information on hibernation.
Returning `{:remove_handler, reply}` sends `reply` as a response to the call,
removes the handler from the `GenEvent` loop and calls `terminate/2` with
reason `:remove_handler` and state `state`.
"""
@callback handle_call(request :: term, state :: term) ::
{:ok, reply, new_state} |
{:ok, reply, new_state, :hibernate} |
{:remove_handler, reply} when reply: term, new_state: term
@doc """
Invoked to handle all other messages. All handlers are run in the `GenEvent`
process so messages intended for other handlers should be ignored with a catch
all clause.
`msg` is the message and `state` is the current state of the handler.
Return values are the same as `handle_event/2`.
"""
@callback handle_info(msg :: term, state :: term) ::
{:ok, new_state} |
{:ok, new_state, :hibernate} |
:remove_handler when new_state: term
@doc """
Invoked when the server is about to exit. It should do any cleanup required.
`reason` is removal reason and `state` is the current state of the handler.
The return value is returned to `GenEvent.remove_handler/3` or ignored if
removing for another reason.
`reason` is one of:
- `:stop` - manager is terminating
- `{:stop, term}` - monitored process terminated (for monitored handlers)
- `:remove_handler` - handler is being removed
- `{:error, term}` - handler crashed or returned a bad value and an error is
logged
- `term` - any term passed to functions like `GenEvent.remove_handler/3`
If part of a supervision tree, a `GenEvent`'s `Supervisor` will send an exit
signal when shutting it down. The exit signal is based on the shutdown
strategy in the child's specification. If it is `:brutal_kill` the `GenEvent`
is killed and so `terminate/2` is not called for its handlers. However if it is
a timeout the `Supervisor` will send the exit signal `:shutdown` and the
`GenEvent` will have the duration of the timeout to call `terminate/2` on all
of its handlers - if the process is still alive after the timeout it is
killed.
If the `GenEvent` receives an exit signal (that is not `:normal`) from any
process when it is not trapping exits it will exit abruptly with the same
reason and so not call the handlers' `terminate/2`. Note that a process does
*NOT* trap exits by default and an exit signal is sent when a linked process
exits or its node is disconnected. Therefore it is not guaranteed that
`terminate/2` is called when a `GenEvent` exits.
Care should be taken to cleanup because the `GenEvent` can continue to loop
after removing the handler. This is different to most other OTP behaviours.
For example if the handler controls a `port` (e.g. `:gen_tcp.socket`) or
`File.io_device`, it will be need to be closed in `terminate/2` as the
process is not exiting so will not be automatically cleaned up.
"""
@callback terminate(reason, state :: term) ::
term when reason: :stop | {:stop, term} | :remove_handler | {:error, term} | term
@doc """
Invoked to change the state of the handler when a different version of the
handler's module is loaded (hot code swapping) and the state's term
structure should be changed.
`old_vsn` is the previous version of the module (defined by the `@vsn`
attribute) when upgrading. When downgrading the previous version is wrapped in
a 2-tuple with first element `:down`. `state` is the current state of the
handler and `extra` is any extra data required to change the state.
Returning `{:ok, new_state}` changes the state to `new_state` and the code
change is successful.
If `code_change/3` raises, the code change fails and the handler will continue
with its previous state. Therefore this callback does not usually contain side
effects.
"""
@callback code_change(old_vsn, state :: term, extra :: term) ::
{:ok, new_state :: term} when old_vsn: term | {:down, term}
@typedoc "Return values of `start*` functions"
@type on_start :: {:ok, pid} | {:error, {:already_started, pid}}
@typedoc "The GenEvent manager name"
@type name :: atom | {:global, term} | {:via, module, term}
@typedoc "Options used by the `start*` functions"
@type options :: [name: name]
@typedoc "The event manager reference"
@type manager :: pid | name | {atom, node}
@typedoc "Supported values for new handlers"
@type handler :: atom | {atom, term}
@doc false
defmacro __using__(_) do
%{file: file, line: line} = __CALLER__
deprecation_message = "the GenEvent module is deprecated, see its documentation for alternatives"
:elixir_errors.warn(line, file, deprecation_message)
quote location: :keep do
@behaviour :gen_event
@@ -100,16 +312,11 @@ defmodule GenEvent do
@doc false
def handle_call(msg, state) do
proc =
case Process.info(self(), :registered_name) do
{_, []} -> self()
{_, name} -> name
end
# We do this to trick Dialyzer to not complain about non-local returns.
reason = {:bad_call, msg}
case :erlang.phash2(1, 1) do
0 -> raise "attempted to call GenEvent #{inspect proc} but no handle_call/2 clause was provided"
1 -> {:remove_handler, {:bad_call, msg}}
0 -> exit(reason)
1 -> {:remove_handler, reason}
end
end
@@ -133,13 +340,33 @@ defmodule GenEvent do
end
end
@doc false
@doc """
Starts an event manager linked to the current process.
This is often used to start the `GenEvent` as part of a supervision tree.
It accepts the `:name` option which is described under the `Name Registration`
section in the `GenServer` module docs.
If the event manager is successfully created and initialized, the function
returns `{:ok, pid}`, where pid is the pid of the server. If a process with
the specified server name already exists, the function returns
`{:error, {:already_started, pid}}` with the pid of that process.
Note that a `GenEvent` started with `start_link/1` is linked to the
parent process and will exit not only on crashes but also if the parent
process exits with `:normal` reason.
"""
@spec start_link(options) :: on_start
def start_link(options \\ []) when is_list(options) do
do_start(:link, options)
end
@doc false
@doc """
Starts an event manager process without links (outside of a supervision tree).
See `start_link/1` for more information.
"""
@spec start(options) :: on_start
def start(options \\ []) when is_list(options) do
do_start(:nolink, options)
@@ -153,45 +380,111 @@ defmodule GenEvent do
:gen.start(GenEvent, mode, @no_callback, [], [])
atom when is_atom(atom) ->
:gen.start(GenEvent, mode, {:local, atom}, @no_callback, [], [])
{:global, _term} = tuple ->
:gen.start(GenEvent, mode, tuple, @no_callback, [], [])
{:via, via_module, _term} = tuple when is_atom(via_module) ->
:gen.start(GenEvent, mode, tuple, @no_callback, [], [])
other ->
raise ArgumentError, """
expected :name option to be one of:
* nil
* atom
* {:global, term}
* {:via, module, term}
Got: #{inspect(other)}
"""
other when is_tuple(other) ->
:gen.start(GenEvent, mode, other, @no_callback, [], [])
end
end
@doc false
@spec stream(manager, keyword) :: GenEvent.Stream.t
@doc """
Returns a stream that consumes events from the `manager`.
The stream is a `GenEvent` struct that implements the `Enumerable`
protocol. Consumption of events only begins when enumeration starts.
Note streaming is specific to Elixir's GenEvent and does not work
with Erlang ones.
## Options
* `:timeout` - raises if no event arrives in X milliseconds
(defaults to `:infinity`)
"""
@spec stream(manager, Keyword.t) :: GenEvent.Stream.t
def stream(manager, options \\ []) do
%GenEvent.Stream{
manager: manager,
timeout: Keyword.get(options, :timeout, :infinity)}
end
@doc false
@doc """
Adds a new event handler to the event `manager`.
The event manager will call the `init/1` callback with `args` to
initiate the event handler and its internal state.
If `init/1` returns a correct value indicating successful completion,
the event manager adds the event handler and this function returns
`:ok`. If the callback fails with `reason` or returns `{:error, reason}`,
the event handler is ignored and this function returns `{:error, reason}`.
If the given handler was previously installed at the manager, this
function returns `{:error, :already_present}`.
For installing multiple instances of the same handler, `{Module, id}` instead
of `Module` must be used. The handler could be then referenced with
`{Module, id}` instead of just `Module`.
"""
@spec add_handler(manager, handler, term) :: :ok | {:error, term}
def add_handler(manager, handler, args) do
rpc(manager, {:add_handler, handler, args})
end
@doc false
@doc """
Adds a monitored event handler to the event `manager`.
Expects the same input and returns the same values as `add_handler/3`.
## Monitored handlers
A monitored handler implies the calling process will now be monitored
by the GenEvent manager.
If the calling process later terminates with `reason`, the event manager
will delete the event handler by calling the `terminate/2` callback with
`{:stop, reason}` as argument. If the event handler later is deleted,
the event manager sends a message `{:gen_event_EXIT, handler, reason}`
to the calling process. Reason is one of the following:
* `:normal` - if the event handler has been removed due to a call to
`remove_handler/3`, or `:remove_handler` has been returned by a callback
function
* `:shutdown` - if the event handler has been removed because the event
manager is terminating
* `{:swapped, new_handler, pid}` - if the process pid has replaced the
event handler by another
* `term` - if the event handler is removed due to an error. Which term
depends on the error
Keep in mind that the `{:gen_event_EXIT, handler, reason}` message is not
guaranteed to be delivered in case the manager crashes. If you want to
guarantee the message is delivered, you have two options:
* monitor the event manager
* link to the event manager and then set `Process.flag(:trap_exit, true)`
in your handler callback
Finally, this functionality only works with GenEvent started via this
module (it is not backwards compatible with Erlang's `:gen_event`).
"""
@spec add_mon_handler(manager, handler, term) :: :ok | {:error, term}
def add_mon_handler(manager, handler, args) do
rpc(manager, {:add_mon_handler, handler, args, self()})
end
@doc false
@doc """
Sends an event notification to the event `manager`.
The event manager will call `handle_event/2` for each
installed event handler.
`notify` is asynchronous and will return immediately after the
notification is sent. `notify` will not fail even if the specified
event manager does not exist, unless it is specified as an atom.
"""
@spec notify(manager, term) :: :ok
def notify(manager, event)
@@ -204,7 +497,7 @@ defmodule GenEvent do
end
end
def notify({:via, mod, name}, msg) when is_atom(mod) do
def notify({:via, mod, name}, msg) do
try do
mod.send(name, {:notify, msg})
:ok
@@ -213,28 +506,50 @@ defmodule GenEvent do
end
end
def notify(manager, msg)
when is_pid(manager)
when is_atom(manager)
when tuple_size(manager) == 2 and
is_atom(elem(manager, 0)) and is_atom(elem(manager, 1)) do
send(manager, {:notify, msg})
def notify(other, msg) do
send(other, {:notify, msg})
:ok
end
@doc false
@doc """
Sends a sync event notification to the event `manager`.
In other words, this function only returns `:ok` after the event manager
invokes the `handle_event/2` callback on each installed event handler.
See `notify/2` for more info.
"""
@spec sync_notify(manager, term) :: :ok
def sync_notify(manager, event) do
rpc(manager, {:sync_notify, event})
end
@doc false
@doc """
Sends an ack event notification to the event `manager`.
In other words, this function only returns `:ok` as soon as the
event manager starts processing this event, but it does not wait
for event handlers to process the sent event.
See `notify/2` for more info. Note this function is specific
to Elixir's GenEvent and does not work with Erlang ones.
"""
@spec ack_notify(manager, term) :: :ok
def ack_notify(manager, event) do
rpc(manager, {:ack_notify, event})
end
@doc false
@doc """
Makes a synchronous call to the event `handler` installed in `manager`.
The given `request` is sent and the caller waits until a reply arrives or
a timeout occurs. The event manager will call `handle_call/2` to handle
the request.
The return value `reply` is defined in the return value of `handle_call/2`.
If the specified event handler is not installed, the function returns
`{:error, :not_found}`.
"""
@spec call(manager, handler, term, timeout) :: term | {:error, term}
def call(manager, handler, request, timeout \\ 5000) do
try do
@@ -247,31 +562,70 @@ defmodule GenEvent do
end
end
@doc false
@doc """
Removes an event handler from the event `manager`.
The event manager will call `terminate/2` to terminate the event handler
and return the callback value. If the specified event handler is not
installed, the function returns `{:error, :not_found}`.
"""
@spec remove_handler(manager, handler, term) :: term | {:error, term}
def remove_handler(manager, handler, args) do
rpc(manager, {:delete_handler, handler, args})
end
@doc false
@doc """
Replaces an old event handler with a new one in the event `manager`.
First, the old event handler is deleted by calling `terminate/2` with
the given `args1` and collects the return value. Then the new event handler
is added and initiated by calling `init({args2, term})`, where `term` is the
return value of calling `terminate/2` in the old handler. This makes it
possible to transfer information from one handler to another.
The new handler will be added even if the specified old event handler
is not installed or if the handler fails to terminate with a given reason
in which case `state = {:error, term}`.
If `init/1` in the second handler returns a correct value, this
function returns `:ok`.
"""
@spec swap_handler(manager, handler, term, handler, term) :: :ok | {:error, term}
def swap_handler(manager, handler1, args1, handler2, args2) do
rpc(manager, {:swap_handler, handler1, args1, handler2, args2})
end
@doc false
@doc """
Replaces an old event handler with a new monitored one in the event `manager`.
Read the docs for `add_mon_handler/3` and `swap_handler/5` for more information.
"""
@spec swap_mon_handler(manager, handler, term, handler, term) :: :ok | {:error, term}
def swap_mon_handler(manager, handler1, args1, handler2, args2) do
rpc(manager, {:swap_mon_handler, handler1, args1, handler2, args2, self()})
end
@doc false
@doc """
Returns a list of all event handlers installed in the `manager`.
"""
@spec which_handlers(manager) :: [handler]
def which_handlers(manager) do
rpc(manager, :which_handlers)
end
@doc false
@doc """
Stops the manager with the given `reason`.
Before terminating, the event manager will call
`terminate(:stop, ...)` for each installed event handler.
It returns `:ok` if the manager terminates with the given
reason, if it terminates with another reason, the call will
exit.
This function keeps OTP semantics regarding error reporting.
If the reason is any other than `:normal`, `:shutdown` or
`{:shutdown, _}`, an error report will be logged.
"""
@spec stop(manager, reason :: term, timeout) :: :ok
def stop(manager, reason \\ :normal, timeout \\ :infinity) do
:gen.stop(manager, reason, timeout)
@@ -292,7 +646,7 @@ defmodule GenEvent do
init_it(starter, self(), name, mod, args, options)
end
def init_it(starter, parent, name, _mod, _args, options) do
def init_it(starter, parent, name, _, _, options) do
Process.put(:"$initial_call", {__MODULE__, :init_it, 6})
debug =
if function_exported?(:gen, :debug_options, 2) do
@@ -773,7 +1127,7 @@ defmodule GenEvent do
defp report_error(handler, reason, state, last_in, name) do
reason =
case reason do
{:undef, [{m, f, a, _} | _] = mfas} ->
{:undef, [{m, f, a, _} | _]=mfas} ->
cond do
:code.is_loaded(m) === false ->
{:"module could not be loaded", mfas}
+11 -2
View File
@@ -1,5 +1,14 @@
defmodule GenEvent.Stream do
@moduledoc false
@moduledoc """
Defines a `GenEvent` stream.
This is a struct returned by `GenEvent.stream/2`. The struct is public and
contains the following fields:
* `:manager` - the manager reference given to `GenEvent.stream/2`
* `:timeout` - the timeout between events, defaults to `:infinity`
"""
defstruct manager: nil, timeout: :infinity
@type t :: %__MODULE__{
@@ -137,7 +146,7 @@ defimpl Enumerable, for: GenEvent.Stream do
# If we reach this branch, the handler was not removed yet,
# so we trigger a request for doing so.
defp stop(stream, {pid, ref, _} = acc) do
_ = :gen_event.delete_handler(pid, {pid, ref}, :shutdown)
_ = GenEvent.remove_handler(pid, {pid, ref}, :shutdown)
stop(stream, {:removed, acc})
end
+73 -245
View File
@@ -11,8 +11,8 @@ defmodule GenServer do
## Example
The GenServer behaviour abstracts the common client-server interaction.
Developers are only required to implement the callbacks and functionality
they are interested in.
Developers are only required to implement the callbacks and functionality they are
interested in.
Let's start with a code example and then explore the available callbacks.
Imagine we want a GenServer that works like a stack, allowing us to push
@@ -53,31 +53,16 @@ defmodule GenServer do
while **cast** messages do not.
Every time you do a `GenServer.call/3`, the client will send a message
that must be handled by the `c:handle_call/3` callback in the GenServer.
A `cast/2` message must be handled by `c:handle_cast/2`.
that must be handled by the `handle_call/3` callback in the GenServer.
A `cast/2` message must be handled by `handle_cast/2`.
## use GenServer and callbacks
## Callbacks
There are 6 callbacks required to be implemented in a `GenServer`. By
adding `use GenServer` to your module, Elixir will automatically define
all 6 callbacks for you, leaving it up to you to implement the ones
you want to customize.
`use GenServer` also defines a `child_spec/1` function, allowing the
defined module to be put under a supervision tree. The generated
`child_spec/1` can be customized with the following options:
* `:id` - the child specification id, defauts to the current module
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
* `:restart` - when the child should be restarted, defaults to `:permanent`
* `:shutdown` - how to shut down the child
For example:
use GenServer, restart: :transient, shutdown: 10_000
See the `Supervisor` docs for more information.
## Name Registration
Both `start_link/3` and `start/3` support the `GenServer` to register
@@ -88,18 +73,16 @@ defmodule GenServer do
using `Process.register/2`.
* `{:global, term}`- the GenServer is registered globally with the given
term using the functions in the [`:global` module](http://www.erlang.org/doc/man/global.html).
term using the functions in the `:global` module.
* `{:via, module, term}` - the GenServer is registered with the given
mechanism and name. The `:via` option expects a module that exports
`register_name/2`, `unregister_name/1`, `whereis_name/1` and `send/2`.
One such example is the [`:global` module](http://www.erlang.org/doc/man/global.html) which uses these functions
for keeping the list of names of processes and their associated PIDs
that are available globally for a network of Elixir nodes. Elixir also
ships with a local, decentralized and scalable registry called `Registry`
for locally storing names that are generated dynamically.
One such example is the `:global` module which uses these functions
for keeping the list of names of processes and their associated pid's
that are available globally for a network of Erlang nodes.
For example, we could start and register our `Stack` server locally as follows:
For example, we could start and register our Stack server locally as follows:
# Start the server and register it locally with name MyStack
{:ok, _} = GenServer.start_link(Stack, [:hello], name: MyStack)
@@ -118,11 +101,6 @@ defmodule GenServer do
* `{:via, module, name}` if the server is registered through an alternative
registry
If there is an interest to register dynamic names locally, do not use
atoms, as atoms are never garbage collected and therefore dynamically
generated atoms won't be garbage collected. For such cases, you can
set up your own local registry by using the `Registry` module.
## Client / Server APIs
Although in the example above we have used `GenServer.start_link/3` and
@@ -173,113 +151,13 @@ defmodule GenServer do
the same module. If the server and/or client implementations are growing
complex, you may want to have them in different modules.
## Receiving "regular" messages
## Receiving custom messages
The goal of a `GenServer` is to abstract the "receive" loop for developers,
automatically handling system messages, support code change, synchronous
calls and more. Therefore, you should never call your own "receive" inside
the GenServer callbacks as doing so will cause the GenServer to misbehave.
Besides the synchronous and asynchronous communication provided by `call/3`
and `cast/2`, "regular" messages sent by functions such `Kernel.send/2`,
`Process.send_after/4` and similar, can be handled inside the `c:handle_info/2`
callback.
`c:handle_info/2` can be used in many situations, such as handling monitor
DOWN messages sent by `Process.monitor/1`. Another use case for `c:handle_info/2`
is to perform periodic work, with the help of `Process.send_after/4`:
defmodule MyApp.Periodically do
use GenServer
def start_link do
GenServer.start_link(__MODULE__, %{})
end
def init(state) do
schedule_work() # Schedule work to be performed on start
{:ok, state}
end
def handle_info(:work, state) do
# Do the desired work here
schedule_work() # Reschedule once more
{:noreply, state}
end
defp schedule_work() do
Process.send_after(self(), :work, 2 * 60 * 60 * 1000) # In 2 hours
end
end
## Debugging with the :sys module
GenServers, as [special processes](http://erlang.org/doc/design_principles/spec_proc.html),
can be debugged using the [`:sys` module](http://www.erlang.org/doc/man/sys.html). Through various hooks, this module
allows developers to introspect the state of the process and trace
system events that happen during its execution, such as received messages,
sent replies and state changes.
Let's explore the basic functions from the [`:sys` module](http://www.erlang.org/doc/man/sys.html) used for debugging:
* [`:sys.get_state/2`](http://erlang.org/doc/man/sys.html#get_state-2) -
allows retrieval of the state of the process. In the case of
a GenServer process, it will be the callback module state, as
passed into the callback functions as last argument.
* [`:sys.get_status/2`](http://erlang.org/doc/man/sys.html#get_status-2) -
allows retrieval of the status of the process. This status includes
the process dictionary, if the process is running or is suspended,
the parent PID, the debugger state, and the state of the behaviour module,
which includes the callback module state (as returned by `:sys.get_state/2`).
It's possible to change how this status is represented by defining
the optional `c:GenServer.format_status/2` callback.
* [`:sys.trace/3`](http://erlang.org/doc/man/sys.html#trace-3) -
prints all the system events to `:stdio`.
* [`:sys.statistics/3`](http://erlang.org/doc/man/sys.html#statistics-3) -
manages collection of process statistics.
* [`:sys.no_debug/2`](http://erlang.org/doc/man/sys.html#no_debug-2) -
turns off all debug handlers for the given process. It is very important
to switch off debugging once we're done. Excessive debug handlers or
those that should be turned off, but weren't, can seriously damage
the performance of the system.
* [`:sys.suspend/2`](http://erlang.org/doc/man/sys.html#suspend-2) - allows
to suspend a process so that it only replies to system messages but no
other messages. A suspended process can be reactivated via
[`:sys.resume/2`](http://erlang.org/doc/man/sys.html#resume-2).
Let's see how we could use those functions for debugging the stack server
we defined earlier.
iex> {:ok, pid} = Stack.start_link([])
iex> :sys.statistics(pid, true) # turn on collecting process statistics
iex> :sys.trace(pid, true) # turn on event printing
iex> Stack.push(pid, 1)
*DBG* <0.122.0> got cast {push,1}
*DBG* <0.122.0> new state [1]
:ok
iex> :sys.get_state(pid)
[1]
iex> Stack.pop(pid)
*DBG* <0.122.0> got call pop from <0.80.0>
*DBG* <0.122.0> sent 1 to <0.80.0>, new state []
1
iex> :sys.statistics(pid, :get)
{:ok,
[start_time: {{2016, 7, 16}, {12, 29, 41}},
current_time: {{2016, 7, 16}, {12, 29, 50}},
reductions: 117, messages_in: 2, messages_out: 0]}
iex> :sys.no_debug(pid) # turn off all debug handlers
:ok
iex> :sys.get_status(pid)
{:status, #PID<0.122.0>, {:module, :gen_server},
[["$initial_call": {Stack, :init, 1}, # pdict
"$ancestors": [#PID<0.80.0>, #PID<0.51.0>]],
:running, # :running | :suspended
#PID<0.80.0>, # parent
[], # debugger state
[header: 'Status for generic server <0.122.0>', # module status
data: [{'Status', :running}, {'Parent', #PID<0.80.0>},
{'Logged events', []}], data: [{'State', [1]}]]]}
If you want to receive custom messages, always receive them in `handle_info/2`.
## Learn more
@@ -294,7 +172,7 @@ defmodule GenServer do
"""
@doc """
Invoked when the server is started. `start_link/3` or `start/3` will
Invoked when the server is started. `start_link/3` (or `start/3`) will
block until it returns.
`args` is the argument term (second argument) passed to `start_link/3`.
@@ -308,10 +186,10 @@ defmodule GenServer do
Returning `{:ok, state, :hibernate}` is similar to
`{:ok, state}` except the process is hibernated before entering the loop. See
`c:handle_call/3` for more information on hibernation.
`handle_call/3` for more information on hibernation.
Returning `:ignore` will cause `start_link/3` to return `:ignore` and the
process will exit normally without entering the loop or calling `c:terminate/2`.
process will exit normally without entering the loop or calling `terminate/2`.
If used when part of a supervision tree the parent supervisor will not fail
to start nor immediately try to restart the `GenServer`. The remainder of the
supervision tree will be (re)started and so the `GenServer` should not be
@@ -326,7 +204,7 @@ defmodule GenServer do
Returning `{:stop, reason}` will cause `start_link/3` to return
`{:error, reason}` and the process to exit with reason `reason` without
entering the loop or calling `c:terminate/2`.
entering the loop or calling `terminate/2`.
"""
@callback init(args :: term) ::
{:ok, state} |
@@ -339,7 +217,7 @@ defmodule GenServer do
reply is received (unless the call times out or nodes are disconnected).
`request` is the request message sent by a `call/3`, `from` is a 2-tuple
containing the caller's PID and a term that uniquely identifies the call, and
containing the caller's pid and a term that uniquely identifies the call, and
`state` is the current state of the `GenServer`.
Returning `{:reply, reply, new_state}` sends the response `reply` to the
@@ -381,7 +259,7 @@ defmodule GenServer do
`{:noreply, new_state}` except a timeout or hibernation occurs as with a
`:reply` tuple.
Returning `{:stop, reason, reply, new_state}` stops the loop and `c:terminate/2`
Returning `{:stop, reason, reply, new_state}` stops the loop and `terminate/2`
is called with reason `reason` and state `new_state`. Then the `reply` is sent
as the response to call and the process exits with reason `reason`.
@@ -413,9 +291,9 @@ defmodule GenServer do
Returning `{:noreply, new_state, :hibernate}` is similar to
`{:noreply, new_state}` except the process is hibernated before continuing the
loop. See `c:handle_call/3` for more information.
loop. See `handle_call/3` for more information.
Returning `{:stop, reason, new_state}` stops the loop and `c:terminate/2` is
Returning `{:stop, reason, new_state}` stops the loop and `terminate/2` is
called with the reason `reason` and state `new_state`. The process exits with
reason `reason`.
@@ -433,7 +311,7 @@ defmodule GenServer do
`msg` is the message and `state` is the current state of the `GenServer`. When
a timeout occurs the message is `:timeout`.
Return values are the same as `c:handle_cast/2`.
Return values are the same as `handle_cast/2`.
If this callback is not implemented, the default implementation by
`use GenServer` will return `{:noreply, state}`.
@@ -449,38 +327,33 @@ defmodule GenServer do
`reason` is exit reason and `state` is the current state of the `GenServer`.
The return value is ignored.
`c:terminate/2` is called if a callback (except `c:init/1`) does one of the
following:
`terminate/2` is called if a callback (except `init/1`) returns a `:stop`
tuple, raises, calls `Kernel.exit/1` or returns an invalid value. It may also
be called if the `GenServer` traps exits using `Process.flag/2` *and* the
parent process sends an exit signal.
* returns a `:stop` tuple
* raises
* calls `Kernel.exit/1`
* returns an invalid value
* the `GenServer` traps exits (using `Process.flag/2`) *and* the parent
process sends an exit signal
If part of a supervision tree, a `GenServer`'s `Supervisor` will send an exit
If part of a supervision tree a `GenServer`'s `Supervisor` will send an exit
signal when shutting it down. The exit signal is based on the shutdown
strategy in the child's specification. If it is `:brutal_kill` the `GenServer`
is killed and so `c:terminate/2` is not called. However if it is a timeout the
is killed and so `terminate/2` is not called. However if it is a timeout the
`Supervisor` will send the exit signal `:shutdown` and the `GenServer` will
have the duration of the timeout to call `c:terminate/2` - if the process is
have the duration of the timeout to call `terminate/2` - if the process is
still alive after the timeout it is killed.
If the `GenServer` receives an exit signal (that is not `:normal`) from any
process when it is not trapping exits it will exit abruptly with the same
reason and so not call `c:terminate/2`. Note that a process does *NOT* trap
reason and so not call `terminate/2`. Note that a process does *NOT* trap
exits by default and an exit signal is sent when a linked process exits or its
node is disconnected.
Therefore it is not guaranteed that `c:terminate/2` is called when a `GenServer`
Therefore it is not guaranteed that `terminate/2` is called when a `GenServer`
exits. For such reasons, we usually recommend important clean-up rules to
happen in separated processes either by use of monitoring or by links
themselves. For example if the `GenServer` controls a `port` (e.g.
`:gen_tcp.socket`) or `t:File.io_device/0`, they will be closed on receiving a
`GenServer`'s exit signal and do not need to be closed in `c:terminate/2`.
`:gen_tcp.socket`) or `File.io_device`, they will be closed on receiving a
`GenServer`'s exit signal and do not need to be closed in `terminate/2`.
If `reason` is not `:normal`, `:shutdown`, nor `{:shutdown, term}` an error is
If `reason` is not `:normal`, `:shutdown` nor `{:shutdown, term}` an error is
logged.
"""
@callback terminate(reason, state :: term) ::
@@ -502,7 +375,7 @@ defmodule GenServer do
Returning `{:error, reason}` fails the code change with reason `reason` and
the state remains as the previous state.
If `c:code_change/3` raises the code change fails and the loop will continue
If `code_change/3` raises the code change fails and the loop will continue
with its previous state. Therefore this callback does not usually contain side effects.
"""
@callback code_change(old_vsn, state :: term, extra :: term) ::
@@ -514,7 +387,7 @@ defmodule GenServer do
This callback can be useful to control the *appearance* of the status of the
`GenServer`. For example, it can be used to return a compact representation of
the `GenServer`'s state to avoid having large state terms printed.
the `GenServers`'s state to avoid having large state terms printed.
* one of `:sys.get_status/1` or `:sys.get_status/2` is invoked to get the
status of the `GenServer`; in such cases, `reason` is `:normal`
@@ -555,31 +428,16 @@ defmodule GenServer do
@typedoc """
Tuple describing the client of a call request.
`pid` is the PID of the caller and `tag` is a unique term used to identify the
`pid` is the pid of the caller and `tag` is a unique term used to identify the
call.
"""
@type from :: {pid, tag :: term}
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
defmacro __using__(_) do
quote location: :keep do
@behaviour GenServer
spec = [
id: opts[:id] || __MODULE__,
start: Macro.escape(opts[:start]) || quote(do: {__MODULE__, :start_link, [arg]}),
restart: opts[:restart] || :permanent,
shutdown: opts[:shutdown] || 5000,
type: :worker
]
@doc false
def child_spec(arg) do
%{unquote_splicing(spec)}
end
defoverridable child_spec: 1
@doc false
def init(args) do
{:ok, args}
@@ -587,43 +445,26 @@ defmodule GenServer do
@doc false
def handle_call(msg, _from, state) do
proc =
case Process.info(self(), :registered_name) do
{_, []} -> self()
{_, name} -> name
end
# We do this to trick Dialyzer to not complain about non-local returns.
reason = {:bad_call, msg}
case :erlang.phash2(1, 1) do
0 -> raise "attempted to call GenServer #{inspect proc} but no handle_call/3 clause was provided"
1 -> {:stop, {:bad_call, msg}, state}
0 -> exit(reason)
1 -> {:stop, reason, state}
end
end
@doc false
def handle_info(msg, state) do
proc =
case Process.info(self(), :registered_name) do
{_, []} -> self()
{_, name} -> name
end
:error_logger.error_msg('~p ~p received unexpected message in handle_info/2: ~p~n',
[__MODULE__, proc, msg])
def handle_info(_msg, state) do
{:noreply, state}
end
@doc false
def handle_cast(msg, state) do
proc =
case Process.info(self(), :registered_name) do
{_, []} -> self()
{_, name} -> name
end
# We do this to trick Dialyzer to not complain about non-local returns.
reason = {:bad_cast, msg}
case :erlang.phash2(1, 1) do
0 -> raise "attempted to cast GenServer #{inspect proc} but no handle_cast/2 clause was provided"
1 -> {:stop, {:bad_cast, msg}, state}
0 -> exit(reason)
1 -> {:stop, reason, state}
end
end
@@ -637,7 +478,8 @@ defmodule GenServer do
{:ok, state}
end
defoverridable GenServer
defoverridable [init: 1, handle_call: 3, handle_info: 2,
handle_cast: 2, terminate: 2, code_change: 3]
end
end
@@ -646,15 +488,15 @@ defmodule GenServer do
This is often used to start the `GenServer` as part of a supervision tree.
Once the server is started, the `c:init/1` function of the given `module` is
Once the server is started, the `init/1` function of the given `module` is
called with `args` as its arguments to initialize the server. To ensure a
synchronized start-up procedure, this function does not return until `c:init/1`
synchronized start-up procedure, this function does not return until `init/1`
has returned.
Note that a `GenServer` started with `start_link/3` is linked to the
parent process and will exit in case of crashes from the parent. The GenServer
will also exit due to the `:normal` reasons in case it is configured to trap
exits in the `c:init/1` callback.
exits in the `init/1` callback.
## Options
@@ -665,7 +507,8 @@ defmodule GenServer do
milliseconds initializing or it will be terminated and the start function
will return `{:error, :timeout}`
* `:debug` - if present, the corresponding function in the [`:sys` module](http://www.erlang.org/doc/man/sys.html) is invoked
* `:debug` - if present, the corresponding function in the [`:sys`
module](http://www.erlang.org/doc/man/sys.html) is invoked
* `:spawn_opt` - if present, its value is passed as options to the
underlying process as in `Process.spawn/4`
@@ -673,11 +516,11 @@ defmodule GenServer do
## Return values
If the server is successfully created and initialized, this function returns
`{:ok, pid}`, where `pid` is the PID of the server. If a process with the
`{:ok, pid}`, where `pid` is the pid of the server. If a process with the
specified server name already exists, this function returns
`{:error, {:already_started, pid}}` with the PID of that process.
`{:error, {:already_started, pid}}` with the pid of that process.
If the `c:init/1` callback fails with `reason`, this function returns
If the `init/1` callback fails with `reason`, this function returns
`{:error, reason}`. Otherwise, if it returns `{:stop, reason}`
or `:ignore`, the process is terminated and this function returns
`{:error, reason}` or `:ignore`, respectively.
@@ -703,28 +546,15 @@ defmodule GenServer do
:gen.start(:gen_server, link, module, args, opts)
{atom, opts} when is_atom(atom) ->
:gen.start(:gen_server, link, {:local, atom}, module, args, opts)
{{:global, _term} = tuple, opts} ->
:gen.start(:gen_server, link, tuple, module, args, opts)
{{:via, via_module, _term} = tuple, opts} when is_atom(via_module) ->
:gen.start(:gen_server, link, tuple, module, args, opts)
{other, _} ->
raise ArgumentError, """
expected :name option to be one of:
* nil
* atom
* {:global, term}
* {:via, module, term}
Got: #{inspect(other)}
"""
{other, opts} when is_tuple(other) ->
:gen.start(:gen_server, link, other, module, args, opts)
end
end
@doc """
Synchronously stops the server with the given `reason`.
Stops the server with the given `reason`.
The `c:terminate/2` callback of the given `server` will be invoked before
The `terminate/2` callback of the given `server` will be invoked before
exiting. This function returns `:ok` if the server terminates with the
given reason; if it terminates with another reason, the call exits.
@@ -741,7 +571,7 @@ defmodule GenServer do
Makes a synchronous call to the `server` and waits for its reply.
The client sends the given `request` to the server and waits until a reply
arrives or a timeout occurs. `c:handle_call/3` will be called on the server
arrives or a timeout occurs. `handle_call/3` will be called on the server
to handle the request.
`server` can be any of the values described in the "Name registration"
@@ -786,7 +616,7 @@ defmodule GenServer do
is unknown whether the destination `server` successfully
handled the message.
`c:handle_cast/2` will be called on the server to handle
`handle_cast/2` will be called on the server to handle
the request. In case the `server` is on a node which is
not yet connected to the caller one, the call is going to
block until a connection happens. This is different than
@@ -830,7 +660,7 @@ defmodule GenServer do
See `multi_call/4` for more information.
"""
@spec abcast([node], name :: atom, term) :: :abcast
def abcast(nodes \\ [node() | Node.list()], name, request) when is_list(nodes) and is_atom(name) do
def abcast(nodes \\ nodes(), name, request) when is_list(nodes) and is_atom(name) do
msg = cast_msg(request)
_ = for node <- nodes, do: do_send({name, node}, msg)
:abcast
@@ -880,19 +710,19 @@ defmodule GenServer do
"""
@spec multi_call([node], name :: atom, term, timeout) ::
{replies :: [{node, term}], bad_nodes :: [node]}
def multi_call(nodes \\ [node() | Node.list()], name, request, timeout \\ :infinity) do
def multi_call(nodes \\ nodes(), name, request, timeout \\ :infinity) do
:gen_server.multi_call(nodes, name, request, timeout)
end
@doc """
Replies to a client.
This function can be used to explicitly send a reply to a client that called
This function can be used to explicitely send a reply to a client that called
`call/3` or `multi_call/4` when the reply cannot be specified in the return
value of `c:handle_call/3`.
value of `handle_call/3`.
`client` must be the `from` argument (the second argument) accepted by
`c:handle_call/3` callbacks. `reply` is an arbitrary term which will be given
`handle_call/3` callbacks. `reply` is an arbitrary term which will be given
back to the client as the return value of the call.
Note that `reply/2` can be called from any process, not just the GenServer
@@ -910,7 +740,6 @@ defmodule GenServer do
def handle_info({:reply, from}, state) do
GenServer.reply(from, :one_second_has_passed)
{:noreply, state}
end
"""
@@ -928,7 +757,7 @@ defmodule GenServer do
@doc """
Returns the `pid` or `{name, node}` of a GenServer process, or `nil` if
no process is associated with the given `server`.
no process is associated with the given name.
## Examples
@@ -940,33 +769,32 @@ defmodule GenServer do
"""
@spec whereis(server) :: pid | {atom, node} | nil
def whereis(server)
def whereis(pid) when is_pid(pid), do: pid
def whereis(name) when is_atom(name) do
Process.whereis(name)
end
def whereis({:global, name}) do
case :global.whereis_name(name) do
pid when is_pid(pid) -> pid
:undefined -> nil
end
end
def whereis({:via, mod, name}) do
case apply(mod, :whereis_name, [name]) do
pid when is_pid(pid) -> pid
:undefined -> nil
end
end
def whereis({name, local}) when is_atom(name) and local == node() do
Process.whereis(name)
end
def whereis({name, node} = server) when is_atom(name) and is_atom(node) do
server
end
@compile {:inline, [nodes: 0]}
defp nodes do
[node() | :erlang.nodes()]
end
end
+7 -29
View File
@@ -5,8 +5,7 @@ defmodule HashDict do
Use the `Map` module instead.
"""
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate every function by 1.4
use Dict
@@ -221,35 +220,16 @@ defmodule HashDict do
end
defimpl Enumerable, for: HashDict do
def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
module.reduce(dict, acc, fun)
end
def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end
def member?(_dict, _) do
{:ok, false}
end
def count(dict) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
{:ok, module.size(dict)}
end
def reduce(dict, acc, fun), do: HashDict.reduce(dict, acc, fun)
def member?(dict, {k, v}), do: {:ok, match?({:ok, ^v}, HashDict.fetch(dict, k))}
def member?(_dict, _), do: {:ok, false}
def count(dict), do: {:ok, HashDict.size(dict)}
end
defimpl Collectable, for: HashDict do
def into(original) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
{original, fn
dict, {:cont, {key, value}} -> module.put(dict, key, value)
dict, {:cont, {k, v}} -> HashDict.put(dict, k, v)
dict, :done -> dict
_, :halt -> :ok
end}
@@ -260,8 +240,6 @@ defimpl Inspect, for: HashDict do
import Inspect.Algebra
def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated.
module = HashDict
concat ["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"]
concat ["#HashDict<", Inspect.List.inspect(HashDict.to_list(dict), opts), ">"]
end
end
+6 -25
View File
@@ -5,8 +5,7 @@ defmodule HashSet do
Use the `MapSet` module instead.
"""
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate every function by 1.4
@node_bitmap 0b111
@node_shift 3
@@ -236,31 +235,15 @@ defmodule HashSet do
end
defimpl Enumerable, for: HashSet do
def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
module.reduce(set, acc, fun)
end
def member?(set, term) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
{:ok, module.member?(set, term)}
end
def count(set) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
{:ok, module.size(set)}
end
def reduce(set, acc, fun), do: HashSet.reduce(set, acc, fun)
def member?(set, v), do: {:ok, HashSet.member?(set, v)}
def count(set), do: {:ok, HashSet.size(set)}
end
defimpl Collectable, for: HashSet do
def into(original) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
{original, fn
set, {:cont, term} -> module.put(set, term)
set, {:cont, x} -> HashSet.put(set, x)
set, :done -> set
_, :halt -> :ok
end}
@@ -271,8 +254,6 @@ defimpl Inspect, for: HashSet do
import Inspect.Algebra
def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated.
module = HashSet
concat ["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"]
concat ["#HashSet<", Inspect.List.inspect(HashSet.to_list(set), opts), ">"]
end
end
+198 -227
View File
@@ -27,7 +27,7 @@ defprotocol Inspect do
end
end
The `concat/1` function comes from `Inspect.Algebra` and it
The `concat` function comes from `Inspect.Algebra` and it
concatenates algebra documents together. In the example above,
it is concatenating the string `"MapSet<"` (all strings are
valid algebra documents that keep their formatting when pretty
@@ -61,52 +61,89 @@ end
defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color(inspect(atom), color_key(atom), opts)
def inspect(atom, _opts) do
inspect(atom)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
defp color_key(nil), do: :nil
defp color_key(_), do: :atom
def inspect(false), do: "false"
def inspect(true), do: "true"
def inspect(nil), do: "nil"
def inspect(:""), do: ":\"\""
def inspect(atom) when is_nil(atom) or is_boolean(atom) do
Atom.to_string(atom)
end
def inspect(atom) when is_atom(atom) do
def inspect(atom) do
binary = Atom.to_string(atom)
case Macro.classify_identifier(atom) do
:alias ->
case binary do
binary when binary in ["Elixir", "Elixir.Elixir"] ->
binary
"Elixir.Elixir." <> _rest ->
binary
"Elixir." <> rest ->
rest
cond do
valid_ref_identifier?(binary) ->
if only_elixir?(binary) do
binary
else
"Elixir." <> rest = binary
rest
end
type when type in [:callable, :not_callable] ->
valid_atom_identifier?(binary) ->
":" <> binary
:other ->
{escaped, _} = Inspect.BitString.escape(binary, ?")
IO.iodata_to_binary [?:, ?", escaped, ?"]
atom in [:%{}, :{}, :<<>>, :..., :%] ->
":" <> binary
atom in Macro.binary_ops or atom in Macro.unary_ops ->
":" <> binary
true ->
<<?:, ?", Inspect.BitString.escape(binary, ?")::binary, ?">>
end
end
defp only_elixir?("Elixir." <> rest), do: only_elixir?(rest)
defp only_elixir?("Elixir"), do: true
defp only_elixir?(_), do: false
# Detect if atom is an atom alias (Elixir.Foo.Bar.Baz)
defp valid_ref_identifier?("Elixir" <> rest) do
valid_ref_piece?(rest)
end
defp valid_ref_identifier?(_), do: false
defp valid_ref_piece?(<<?., h, t::binary>>) when h in ?A..?Z do
valid_ref_piece? valid_identifier?(t)
end
defp valid_ref_piece?(<<>>), do: true
defp valid_ref_piece?(_), do: false
# Detect if atom
defp valid_atom_identifier?(<<h, t::binary>>) when h in ?a..?z or h in ?A..?Z or h == ?_ do
valid_atom_piece?(t)
end
defp valid_atom_identifier?(_), do: false
defp valid_atom_piece?(t) do
case valid_identifier?(t) do
<<>> -> true
<<??>> -> true
<<?!>> -> true
<<?@, t::binary>> -> valid_atom_piece?(t)
_ -> false
end
end
defp valid_identifier?(<<h, t::binary>>)
when h in ?a..?z
when h in ?A..?Z
when h in ?0..?9
when h == ?_ do
valid_identifier? t
end
defp valid_identifier?(other), do: other
end
defimpl Inspect, for: BitString do
def inspect(term, opts) when is_binary(term) do
%Inspect.Opts{binaries: bins, base: base, printable_limit: printable_limit} = opts
if base == :decimal and
(bins == :as_strings or (bins == :infer and String.printable?(term, printable_limit))) do
inspected =
case escape(term, ?", printable_limit) do
{escaped, ""} -> [?", escaped, ?"]
{escaped, _} -> [?", escaped, ?", " <> ..."]
end
color(IO.iodata_to_binary(inspected), :string, opts)
def inspect(term, %Inspect.Opts{binaries: bins, base: base} = opts) when is_binary(term) do
if base == :decimal and (bins == :as_strings or (bins == :infer and String.printable?(term))) do
<<?", escape(term, ?")::binary, ?">>
else
inspect_bitstring(term, opts)
end
@@ -120,102 +157,96 @@ defimpl Inspect, for: BitString do
@doc false
def escape(other, char) do
escape(other, char, :infinity, [])
escape(other, char, <<>>)
end
@doc false
def escape(other, char, count) do
escape(other, char, count, [])
defp escape(<<char, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, char>>)
end
defp escape(<<_, _::binary>> = binary, _char, 0, acc) do
{acc, binary}
defp escape(<<?#, ?{, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?#, ?{>>)
end
defp escape(<<char, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | [?\\, char]])
defp escape(<<?\a, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?a>>)
end
defp escape(<<?#, ?{, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\\#{'])
defp escape(<<?\b, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?b>>)
end
defp escape(<<?\a, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\a'])
defp escape(<<?\d, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?d>>)
end
defp escape(<<?\b, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\b'])
defp escape(<<?\e, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?e>>)
end
defp escape(<<?\d, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\d'])
defp escape(<<?\f, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?f>>)
end
defp escape(<<?\e, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\e'])
defp escape(<<?\n, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?n>>)
end
defp escape(<<?\f, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\f'])
defp escape(<<?\r, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?r>>)
end
defp escape(<<?\n, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\n'])
defp escape(<<?\\, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?\\>>)
end
defp escape(<<?\r, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\r'])
defp escape(<<?\t, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?t>>)
end
defp escape(<<?\\, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\\\'])
defp escape(<<?\v, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, ?\\, ?v>>)
end
defp escape(<<?\t, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\t'])
defp escape(<<h::utf8, t::binary>>, char, binary) do
head = <<h::utf8>>
if String.printable?(head) do
escape(t, char, append(head, binary))
else
<<byte::8, h::binary>> = head
t = <<h::binary, t::binary>>
escape(t, char, <<binary::binary, escape_char(byte)::binary>>)
end
end
defp escape(<<?\v, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | '\\v'])
end
defp escape(<<h::utf8, t::binary>>, char, count, acc)
when h in 0x20..0x7E
when h in 0xA0..0xD7FF
when h in 0xE000..0xFFFD
when h in 0x10000..0x10FFFF do
escape(t, char, decrement(count), [acc | <<h::utf8>>])
end
defp escape(<<h, t::binary>>, char, count, acc) do
escape(t, char, decrement(count), [acc | escape_char(h)])
end
defp escape(<<>>, _char, _count, acc) do
{acc, <<>>}
defp escape(<<h, t::binary>>, char, binary) do
escape(t, char, <<binary::binary, escape_char(h)::binary>>)
end
defp escape(<<>>, _char, binary), do: binary
@doc false
# Also used by Regex
def escape_char(0) do
'\\0'
<<?\\, ?0>>
end
def escape_char(char) when char < 0x100 do
<<a::4, b::4>> = <<char::8>>
['\\x', to_hex(a), to_hex(b)]
<<?\\, ?x, to_hex(a), to_hex(b)>>
end
def escape_char(char) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
<<?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
end
def escape_char(char) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
['\\x{', to_hex(a), to_hex(b), to_hex(c),
to_hex(d), to_hex(e), to_hex(f), ?}]
<<?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c),
to_hex(d), to_hex(e), to_hex(f), ?}>>
end
defp to_hex(c) when c in 0..9, do: ?0 + c
defp to_hex(c) when c in 10..15, do: ?A + c - 10
defp to_hex(c) when c in 0..9, do: ?0+c
defp to_hex(c) when c in 10..15, do: ?A+c-10
defp append(<<h, t::binary>>, binary), do: append(t, <<binary::binary, h>>)
defp append(<<>>, binary), do: binary
## Bitstrings
defp inspect_bitstring("", opts) do
color("<<>>", :binary, opts)
defp inspect_bitstring("", _opts) do
"<<>>"
end
defp inspect_bitstring(bitstring, opts) do
left = color("<<", :binary, opts)
right = color(">>", :binary, opts)
nest surround(left, each_bit(bitstring, opts.limit, opts), right), 1
nest surround("<<", each_bit(bitstring, opts.limit, opts), ">>"), 1
end
defp each_bit(_, 0, _) do
@@ -241,58 +272,43 @@ defimpl Inspect, for: BitString do
Inspect.Integer.inspect(h, opts) <> "::size(" <> Integer.to_string(size) <> ")"
end
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
end
defimpl Inspect, for: List do
def inspect([], opts) do
color("[]", :list, opts)
end
def inspect([], _opts), do: "[]"
# TODO: Remove :char_list and :as_char_lists handling in 2.0
def inspect(term, opts) do
%Inspect.Opts{charlists: lists, char_lists: lists_deprecated, printable_limit: printable_limit} = opts
# TODO: Deprecate :char_lists and :as_char_lists keys in v1.5
def inspect(term, %Inspect.Opts{charlists: lists, char_lists: lists_deprecated} = opts) do
lists =
if lists == :infer and lists_deprecated != :infer do
case lists_deprecated do
:as_char_lists ->
IO.warn "the :char_lists inspect option and its :as_char_lists " <>
"value are deprecated, use the :charlists option and its " <>
":as_charlists value instead"
:as_charlists
_ ->
IO.warn "the :char_lists inspect option is deprecated, use :charlists instead"
lists_deprecated
end
else
lists
end
open = color("[", :list, opts)
sep = color(",", :list, opts)
close = color("]", :list, opts)
cond do
lists == :as_charlists or (lists == :infer and printable?(term, printable_limit)) ->
inspected =
case Inspect.BitString.escape(IO.chardata_to_string(term), ?', printable_limit) do
{escaped, ""} -> [?', escaped, ?']
{escaped, _} -> [?', escaped, ?', " ++ ..."]
end
IO.iodata_to_binary inspected
lists == :as_charlists or (lists == :infer and printable?(term)) ->
<<?', Inspect.BitString.escape(IO.chardata_to_string(term), ?')::binary, ?'>>
keyword?(term) ->
surround_many(open, term, close, opts, &keyword/2, sep)
surround_many("[", term, "]", opts, &keyword/2)
true ->
surround_many(open, term, close, opts, &to_doc/2, sep)
surround_many("[", term, "]", opts, &to_doc/2)
end
end
@doc false
def keyword({key, value}, opts) do
key = color(key_to_binary(key) <> ": ", :atom, opts)
concat(key, to_doc(value, opts))
concat(
key_to_binary(key) <> ": ",
to_doc(value, opts)
)
end
@doc false
@@ -307,25 +323,17 @@ defimpl Inspect, for: List do
def keyword?(_other), do: false
@doc false
def printable?(list), do: printable?(list, :infinity)
@doc false
def printable?(_, 0), do: true
def printable?([char | rest], counter) when char in 32..126, do: printable?(rest, decrement(counter))
def printable?([?\n | rest], counter), do: printable?(rest, decrement(counter))
def printable?([?\r | rest], counter), do: printable?(rest, decrement(counter))
def printable?([?\t | rest], counter), do: printable?(rest, decrement(counter))
def printable?([?\v | rest], counter), do: printable?(rest, decrement(counter))
def printable?([?\b | rest], counter), do: printable?(rest, decrement(counter))
def printable?([?\f | rest], counter), do: printable?(rest, decrement(counter))
def printable?([?\e | rest], counter), do: printable?(rest, decrement(counter))
def printable?([?\a | rest], counter), do: printable?(rest, decrement(counter))
def printable?([], _counter), do: true
def printable?(_, _counter), do: false
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
def printable?([c | cs]) when c in 32..126, do: printable?(cs)
def printable?([?\n | cs]), do: printable?(cs)
def printable?([?\r | cs]), do: printable?(cs)
def printable?([?\t | cs]), do: printable?(cs)
def printable?([?\v | cs]), do: printable?(cs)
def printable?([?\b | cs]), do: printable?(cs)
def printable?([?\f | cs]), do: printable?(cs)
def printable?([?\e | cs]), do: printable?(cs)
def printable?([?\a | cs]), do: printable?(cs)
def printable?([]), do: true
def printable?(_), do: false
## Private
@@ -338,11 +346,10 @@ defimpl Inspect, for: List do
end
defimpl Inspect, for: Tuple do
def inspect({}, _opts), do: "{}"
def inspect(tuple, opts) do
open = color("{", :tuple, opts)
sep = color(",", :tuple, opts)
close = color("}", :tuple, opts)
surround_many(open, Tuple.to_list(tuple), close, opts, &to_doc/2, sep)
surround_many("{", Tuple.to_list(tuple), "}", opts, &to_doc/2)
end
end
@@ -353,33 +360,29 @@ defimpl Inspect, for: Map do
def inspect(map, name, opts) do
map = :maps.to_list(map)
open = color("%" <> name <> "{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
surround_many(open, map, close, opts, traverse_fun(map, opts), sep)
surround_many("%" <> name <> "{", map, "}", opts, traverse_fun(map))
end
defp traverse_fun(list, opts) do
defp traverse_fun(list) do
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
sep = color(" => ", :map, opts)
&to_map(&1, &2, sep)
&to_map/2
end
end
defp to_map({key, value}, opts, sep) do
defp to_map({key, value}, opts) do
concat(
concat(to_doc(key, opts), sep),
concat(to_doc(key, opts), " => "),
to_doc(value, opts)
)
end
end
defimpl Inspect, for: Integer do
def inspect(term, %Inspect.Opts{base: base} = opts) do
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
color(inspected, :number, opts)
def inspect(term, %Inspect.Opts{base: base}) do
Integer.to_string(term, base_to_value(base))
|> prepend_prefix(base)
end
defp base_to_value(base) do
@@ -403,54 +406,60 @@ defimpl Inspect, for: Integer do
end
defimpl Inspect, for: Float do
def inspect(term, opts) do
inspected = IO.iodata_to_binary(:io_lib_format.fwrite_g(term))
color(inspected, :number, opts)
def inspect(term, _opts) do
IO.iodata_to_binary(:io_lib_format.fwrite_g(term))
end
end
defimpl Inspect, for: Regex do
def inspect(regex, opts) do
source = IO.iodata_to_binary(['~r/', escape(regex.source, ?/), ?/, regex.opts])
color(source, :regex, opts)
def inspect(regex, _opts) do
delim = ?/
concat ["~r",
<<delim, escape(regex.source, delim)::binary, delim>>,
regex.opts]
end
defp escape(bin, term),
do: escape(bin, [], term)
do: escape(bin, <<>>, term)
defp escape(<<term, rest::binary>>, buf, term),
do: escape(rest, [buf | [?\\, term]], term)
defp escape(<<?\\, term>> <> rest, buf, term),
do: escape(rest, buf <> <<?\\, term>>, term)
# The list of characters is from 'String.printable?' implementation
defp escape(<<term>> <> rest, buf, term),
do: escape(rest, buf <> <<?\\, term>>, term)
# the list of characters is from "String.printable?" impl
# minus characters treated specially by regex: \s, \d, \b, \e
defp escape(<<?\n, rest::binary>>, buf, term),
do: escape(rest, [buf | '\\n'], term)
defp escape(<<?\n>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?n>>, term)
defp escape(<<?\r, rest::binary>>, buf, term),
do: escape(rest, [buf | '\\r'], term)
defp escape(<<?\r>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?r>>, term)
defp escape(<<?\t, rest::binary>>, buf, term),
do: escape(rest, [buf | '\\t'], term)
defp escape(<<?\t>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?t>>, term)
defp escape(<<?\v, rest::binary>>, buf, term),
do: escape(rest, [buf | '\\v'], term)
defp escape(<<?\v>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?v>>, term)
defp escape(<<?\f, rest::binary>>, buf, term),
do: escape(rest, [buf | '\\f'], term)
defp escape(<<?\f>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?f>>, term)
defp escape(<<?\a, rest::binary>>, buf, term),
do: escape(rest, [buf | '\\a'], term)
defp escape(<<?\a>> <> rest, buf, term),
do: escape(rest, <<buf::binary, ?\\, ?a>>, term)
defp escape(<<char::utf8, rest::binary>>, buf, term)
when char in 0x20..0x7E
when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF,
do: escape(rest, [buf | <<char::utf8>>], term)
defp escape(<<c::utf8>> <> rest, buf, term) do
charstr = <<c::utf8>>
if String.printable?(charstr) and not c in [?\d, ?\b, ?\e] do
escape(rest, buf <> charstr, term)
else
escape(rest, buf <> Inspect.BitString.escape_char(c), term)
end
end
defp escape(<<char, rest::binary>>, buf, term),
do: escape(rest, [buf | Inspect.BitString.escape_char(char)], term)
defp escape(<<c>> <> rest, buf, term),
do: escape(rest, <<buf::binary, Inspect.BitString.escape_char(c)>>, term)
defp escape(<<>>, buf, _), do: buf
end
@@ -459,10 +468,9 @@ defimpl Inspect, for: Function do
def inspect(function, _opts) do
fun_info = :erlang.fun_info(function)
mod = fun_info[:module]
name = fun_info[:name]
if fun_info[:type] == :external and fun_info[:env] == [] do
"&#{Inspect.Atom.inspect(mod)}.#{escape_name(name)}/#{fun_info[:arity]}"
"&#{Inspect.Atom.inspect(mod)}.#{fun_info[:name]}/#{fun_info[:arity]}"
else
case Atom.to_charlist(mod) do
'elixir_compiler_' ++ _ ->
@@ -477,41 +485,6 @@ defimpl Inspect, for: Function do
end
end
def escape_name(atom) when is_atom(atom) do
string = Atom.to_string(atom)
case Macro.classify_identifier(atom) do
:callable ->
string
type when type in [:not_callable, :alias] ->
"\"" <> string <> "\""
:other ->
{escaped, _} = Inspect.BitString.escape(string, ?")
IO.iodata_to_binary [?", escaped, ?"]
end
end
# Example of this format: -NAME/ARITY-fun-COUNT-
def extract_anonymous_fun_parent(atom) when is_atom(atom) do
extract_anonymous_fun_parent(Atom.to_string(atom))
end
def extract_anonymous_fun_parent("-" <> rest) do
[trailing | reversed] =
rest
|> String.split("/")
|> Enum.reverse()
case String.split(trailing, "-") do
[arity, _inner, _count, ""] ->
{reversed |> Enum.reverse |> Enum.join("/") |> String.to_atom(), arity}
_other ->
:error
end
end
def extract_anonymous_fun_parent(other) when is_binary(other), do: :error
defp default_inspect(mod, fun_info) do
"#Function<#{uniq(fun_info)}/#{fun_info[:arity]} in " <>
"#{Inspect.Atom.inspect(mod)}#{extract_name(fun_info[:name])}>"
@@ -522,11 +495,10 @@ defimpl Inspect, for: Function do
end
defp extract_name(name) do
case extract_anonymous_fun_parent(name) do
{name, arity} ->
"." <> escape_name(name) <> "/" <> arity
:error ->
"." <> escape_name(name)
name = Atom.to_string(name)
case :binary.split(name, "-", [:global]) do
["", name | _] -> "." <> name
_ -> "." <> name
end
end
@@ -544,7 +516,7 @@ end
defimpl Inspect, for: Port do
def inspect(port, _opts) do
IO.iodata_to_binary(:erlang.port_to_list(port))
IO.iodata_to_binary :erlang.port_to_list(port)
end
end
@@ -565,8 +537,7 @@ defimpl Inspect, for: Any do
dunder ->
if :maps.keys(dunder) == :maps.keys(map) do
pruned = :maps.remove(:__exception__, :maps.remove(:__struct__, map))
colorless_opts = %{opts | syntax_colors: []}
Inspect.Map.inspect(pruned, Inspect.Atom.inspect(struct, colorless_opts), opts)
Inspect.Map.inspect(pruned, Inspect.Atom.inspect(struct, opts), opts)
else
Inspect.Map.inspect(map, opts)
end
+96 -196
View File
@@ -24,11 +24,8 @@ defmodule Inspect.Opts do
is printable, otherwise as list.
* `:limit` - limits the number of items that are printed for tuples,
bitstrings, maps, lists and any other collection of items. It does not
apply to strings nor charlists and defaults to 50.
* `:printable_limit` - limits the number of bytes that are printed for strings
and char lists. Defaults to 4096.
bitstrings, and lists, does not apply to strings nor charlists, defaults
to 50.
* `:pretty` - if set to `true` enables pretty printing, defaults to `false`.
@@ -36,51 +33,39 @@ defmodule Inspect.Opts do
printing to IO devices. Set to 0 to force each item to be printed on its
own line.
* `:base` - prints integers as `:binary`, `:octal`, `:decimal`, or `:hex`,
defaults to `:decimal`. When inspecting binaries any `:base` other than
`:decimal` implies `binaries: :as_binaries`.
* `:base` - print integers as :binary, :octal, :decimal, or :hex, defaults
to :decimal. When inspecting binaries any `:base` other than `:decimal`
implies `binaries: :as_binaries`.
* `:safe` - when `false`, failures while inspecting structs will be raised
as errors instead of being wrapped in the `Inspect.Error` exception. This
is useful when debugging failures and crashes for custom inspect
implementations
* `:syntax_colors` - when set to a keyword list of colors the output will
be colorized. The keys are types and the values are the colors to use for
each type. e.g. `[number: :red, atom: :blue]`. Types can include
`:number`, `:atom`, `regex`, `:tuple`, `:map`, `:list`, and `:reset`.
Colors can be any `t:IO.ANSI.ansidata/0` as accepted by `IO.ANSI.format/1`.
"""
# TODO: Remove :char_lists key by 2.0
# TODO: Deprecate char_lists key by v1.5
defstruct structs: true,
binaries: :infer,
charlists: :infer,
char_lists: :infer,
limit: 50,
printable_limit: 4096,
width: 80,
base: :decimal,
pretty: false,
safe: true,
syntax_colors: []
safe: true
@type color_key :: atom
# TODO: Remove :char_lists key and :as_char_lists value by 2.0
# TODO: Deprecate char_lists key and :as_char_lists value by v1.5
@type t :: %__MODULE__{
structs: boolean,
binaries: :infer | :as_binaries | :as_strings,
charlists: :infer | :as_lists | :as_charlists,
char_lists: :infer | :as_lists | :as_char_lists,
limit: pos_integer | :infinity,
printable_limit: pos_integer | :infinity,
width: pos_integer | :infinity,
base: :decimal | :binary | :hex | :octal,
pretty: boolean,
safe: boolean,
syntax_colors: [{color_key, IO.ANSI.ansidata}]
}
safe: boolean}
end
defmodule Inspect.Error do
@@ -115,7 +100,7 @@ defmodule Inspect.Algebra do
The functions `nest/2`, `space/2` and `line/2` help you put the
document together into a rigid structure. However, the document
algebra gets interesting when using functions like `break/1`, which
algebra gets interesting when using functions like `break/2`, which
converts the given string into a line break depending on how much space
there is to print. Let's glue two docs together with a break and then
render it:
@@ -164,11 +149,11 @@ defmodule Inspect.Algebra do
@tail_separator " |"
@newline "\n"
@nesting 1
@space " "
@break " "
# Functional interface to "doc" records
@type t :: :doc_nil | :doc_line | doc_cons | doc_nest | doc_break | doc_group | doc_color | binary
@type t :: :doc_nil | :doc_line | doc_cons | doc_nest | doc_break | doc_group | binary
@typep doc_cons :: {:doc_cons, t, t}
defmacrop doc_cons(left, right) do
@@ -177,7 +162,7 @@ defmodule Inspect.Algebra do
@typep doc_nest :: {:doc_nest, t, non_neg_integer}
defmacrop doc_nest(doc, indent) do
quote do: {:doc_nest, unquote(doc), unquote(indent)}
quote do: {:doc_nest, unquote(doc), unquote(indent) }
end
@typep doc_break :: {:doc_break, binary}
@@ -190,11 +175,6 @@ defmodule Inspect.Algebra do
quote do: {:doc_group, unquote(group)}
end
@typep doc_color :: {:doc_color, t, IO.ANSI.ansidata}
defmacrop doc_color(doc, color) do
quote do: {:doc_color, unquote(doc), unquote(color)}
end
defmacrop is_doc(doc) do
if Macro.Env.in_guard?(__CALLER__) do
do_is_doc(doc)
@@ -212,17 +192,15 @@ defmodule Inspect.Algebra do
is_binary(unquote(doc)) or
unquote(doc) in [:doc_nil, :doc_line] or
(is_tuple(unquote(doc)) and
elem(unquote(doc), 0) in [:doc_cons, :doc_nest, :doc_break, :doc_group, :doc_color])
elem(unquote(doc), 0) in [:doc_cons, :doc_nest, :doc_break, :doc_group])
end
end
@doc """
Converts an Elixir term to an algebra document
according to the `Inspect` protocol.
Converts an Elixir structure to an algebra document
according to the inspect protocol.
"""
@spec to_doc(any, Inspect.Opts.t) :: t
def to_doc(term, opts)
def to_doc(%{__struct__: struct} = map, %Inspect.Opts{} = opts) when is_atom(struct) do
if opts.structs do
try do
@@ -243,7 +221,7 @@ defmodule Inspect.Algebra do
try do
Process.put(:inspect_trap, true)
res = Inspect.Map.inspect(map, %{opts | syntax_colors: []})
res = Inspect.Map.inspect(map, opts)
res = IO.iodata_to_binary(format(res, :infinity))
exception = Inspect.Error.exception(
@@ -282,51 +260,31 @@ defmodule Inspect.Algebra do
@spec empty() :: :doc_nil
def empty, do: :doc_nil
@doc ~S"""
Concatenates two document entities returning a new document.
@doc """
Concatenates two document entities.
## Examples
iex> doc = Inspect.Algebra.concat("hello", "world")
iex> doc = Inspect.Algebra.concat "hello", "world"
iex> Inspect.Algebra.format(doc, 80)
["hello", "world"]
"""
@spec concat(t, t) :: t
def concat(doc1, doc2) when is_doc(doc1) and is_doc(doc2) do
doc_cons(doc1, doc2)
@spec concat(t, t) :: doc_cons
def concat(x, y) when is_doc(x) and is_doc(y) do
doc_cons(x, y)
end
@doc ~S"""
Concatenates a list of documents returning a new document.
## Examples
iex> doc = Inspect.Algebra.concat(["a", "b", "c"])
iex> Inspect.Algebra.format(doc, 80)
["a", "b", "c"]
@doc """
Concatenates a list of documents.
"""
@spec concat([t]) :: t
def concat(docs) when is_list(docs) do
@spec concat([t]) :: doc_cons
def concat(docs) do
fold_doc(docs, &concat(&1, &2))
end
@doc ~S"""
Colors a document if the `color_key` has a color in the options.
"""
@spec color(t, Inspect.Opts.color_key, Inspect.Opts.t) :: doc_color
def color(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
if precolor = Keyword.get(syntax_colors, color_key) do
postcolor = Keyword.get(syntax_colors, :reset, :reset)
concat(doc_color(doc, precolor), doc_color(empty(), postcolor))
else
doc
end
end
@doc ~S"""
Nests the given document at the given `level`.
Nests document entity `x` positions deep.
Nesting will be appended to the line breaks.
@@ -338,91 +296,58 @@ defmodule Inspect.Algebra do
"""
@spec nest(t, non_neg_integer) :: doc_nest
def nest(doc, level)
def nest(doc, 0) when is_doc(doc) do
doc
def nest(x, 0) when is_doc(x) do
x
end
def nest(doc, level) when is_doc(doc) and is_integer(level) and level > 0 do
doc_nest(doc, level)
def nest(x, i) when is_doc(x) and is_integer(i) do
doc_nest(x, i)
end
@doc ~S"""
Returns a document entity representing a break based on the given
`string`.
Document entity representing a break.
This break can be rendered as a linebreak or as the given `string`,
This break can be rendered as a linebreak or as spaces,
depending on the `mode` of the chosen layout or the provided
separator.
## Examples
Let's create a document by concatenating two strings with a break between
them:
Let's glue two docs together with a break and then render it:
iex> doc = Inspect.Algebra.concat(["a", Inspect.Algebra.break("\t"), "b"])
iex> doc = Inspect.Algebra.glue("a", " ", "b")
iex> Inspect.Algebra.format(doc, 80)
["a", "\t", "b"]
["a", " ", "b"]
Notice the break was represented with the given string, because we didn't
reach a line limit. Once we do, it is replaced by a newline:
Notice the break was represented as is, because we haven't reached
a line limit. Once we do, it is replaced by a newline:
iex> break = Inspect.Algebra.break("\t")
iex> doc = Inspect.Algebra.concat([String.duplicate("a", 20), break, "b"])
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
iex> Inspect.Algebra.format(doc, 10)
["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]
"""
@spec break(binary) :: doc_break
def break(string) when is_binary(string), do: doc_break(string)
def break(s) when is_binary(s), do: doc_break(s)
@doc ~S"""
Returns a document entity with the `" "` string as break.
See `break/1` for more information.
"""
@spec break() :: doc_break
def break(), do: doc_break(@space)
@doc ~S"""
Glues two documents together inserting `" "` as a break between them.
This means the two documents will be separated by `" "` in case they
fit in the same line. Otherwise a line break is used.
## Examples
iex> doc = Inspect.Algebra.glue("hello", "world")
iex> Inspect.Algebra.format(doc, 80)
["hello", " ", "world"]
def break(), do: doc_break(@break)
@doc """
Inserts a break between two docs. See `break/1` for more info.
"""
@spec glue(t, t) :: t
def glue(doc1, doc2), do: concat(doc1, concat(break(), doc2))
@doc ~S"""
Glues two documents (`doc1` and `doc2`) together inserting the given
break `break_string` between them.
For more information on how the break is inserted, see `break/1`.
## Examples
iex> doc = Inspect.Algebra.glue("hello", "\t", "world")
iex> Inspect.Algebra.format(doc, 80)
["hello", "\t", "world"]
@spec glue(t, t) :: doc_cons
def glue(x, y), do: concat(x, concat(break, y))
@doc """
Inserts a break, passed as the second argument, between two docs,
the first and the third arguments.
"""
@spec glue(t, binary, t) :: t
def glue(doc1, break_string, doc2) when is_binary(break_string),
do: concat(doc1, concat(break(break_string), doc2))
@spec glue(t, binary, t) :: doc_cons
def glue(x, g, y) when is_binary(g), do: concat(x, concat(break(g), y))
@doc ~S"""
Returns a group containing the specified document `doc`.
Documents in a group are attempted to be rendered together
to the best of the renderer ability.
Returns a group containing the specified document.
## Examples
@@ -449,91 +374,83 @@ defmodule Inspect.Algebra do
"""
@spec group(t) :: doc_group
def group(doc) when is_doc(doc) do
doc_group(doc)
def group(d) when is_doc(d) do
doc_group(d)
end
@doc ~S"""
Inserts a mandatory single space between two documents.
@doc """
Inserts a mandatory single space between two document entities.
## Examples
iex> doc = Inspect.Algebra.space("Hughes", "Wadler")
iex> Inspect.Algebra.format(doc, 5)
iex> doc = Inspect.Algebra.space "Hughes", "Wadler"
iex> Inspect.Algebra.format(doc, 80)
["Hughes", " ", "Wadler"]
"""
@spec space(t, t) :: t
def space(doc1, doc2), do: concat(doc1, concat(" ", doc2))
@spec space(t, t) :: doc_cons
def space(x, y), do: concat(x, concat(" ", y))
@doc ~S"""
Inserts a mandatory linebreak between two documents.
Inserts a mandatory linebreak between two document entities.
## Examples
iex> doc = Inspect.Algebra.line("Hughes", "Wadler")
iex> doc = Inspect.Algebra.line "Hughes", "Wadler"
iex> Inspect.Algebra.format(doc, 80)
["Hughes", "\n", "Wadler"]
"""
@spec line(t, t) :: t
def line(doc1, doc2), do: concat(doc1, concat(:doc_line, doc2))
@spec line(t, t) :: doc_cons
def line(x, y), do: concat(x, concat(:doc_line, y))
@doc ~S"""
Folds a list of documents into a document using the given folder function.
The list of documents is folded "from the right"; in that, this function is
similar to `List.foldr/3`, except that it doesn't expect an initial
accumulator and uses the last element of `docs` as the initial accumulator.
@doc """
Folds a list of document entities into a document entity
using a function that is passed as the first argument.
## Examples
iex> docs = ["A", "B", "C"]
iex> docs = Inspect.Algebra.fold_doc(docs, fn(doc, acc) ->
...> Inspect.Algebra.concat([doc, "!", acc])
iex> doc = ["A", "B"]
iex> doc = Inspect.Algebra.fold_doc(doc, fn(x, y) ->
...> Inspect.Algebra.concat [x, "!", y]
...> end)
iex> Inspect.Algebra.format(docs, 80)
["A", "!", "B", "!", "C"]
iex> Inspect.Algebra.format(doc, 80)
["A", "!", "B"]
"""
@spec fold_doc([t], ((t, t) -> t)) :: t
def fold_doc(docs, folder_fun)
def fold_doc([], _folder_fun),
do: empty()
def fold_doc([doc], _folder_fun),
do: doc
def fold_doc([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold_doc(docs, folder_fun))
def fold_doc(list, fun)
def fold_doc([], _), do: empty
def fold_doc([doc], _), do: doc
def fold_doc([d | ds], fun), do: fun.(d, fold_doc(ds, fun))
# Elixir conveniences
@doc ~S"""
Surrounds a document with characters.
Puts the given document `doc` between the `left` and `right` documents enclosing
and nesting it. The document is marked as a group, to show the maximum as
possible concisely together.
Puts the document between left and right enclosing and nesting it.
The document is marked as a group, to show the maximum as possible
concisely together.
## Examples
iex> doc = Inspect.Algebra.surround("[", Inspect.Algebra.glue("a", "b"), "]")
iex> doc = Inspect.Algebra.surround "[", Inspect.Algebra.glue("a", "b"), "]"
iex> Inspect.Algebra.format(doc, 3)
["[", "a", "\n ", "b", "]"]
"""
@spec surround(t, t, t) :: t
def surround(left, doc, right) when is_doc(left) and is_doc(doc) and is_doc(right) do
group(concat(left, concat(nest(doc, @nesting), right)))
@spec surround(binary, t, binary) :: t
def surround(left, doc, right) do
group concat left, concat(nest(doc, @nesting), right)
end
@doc ~S"""
Maps and glues a collection of items.
It uses the given `left` and `right` documents as surrounding and the
separator document `separator` to separate items in `docs`. A limit can be
passed: when this limit is reached, this function stops gluing and outputs
`"..."` instead.
It uses the given left and right as surrounding and a separator for
each item. A limit can be passed which, once reached, stops gluing
and outputs "..." instead.
## Examples
@@ -551,11 +468,9 @@ defmodule Inspect.Algebra do
...> %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end, "!")
iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
"[1! 2! 3! ...]"
"""
@spec surround_many(t, [any], t, Inspect.Opts.t, (term, Inspect.Opts.t -> t), t) :: t
def surround_many(left, docs, right, %Inspect.Opts{} = opts, fun, separator \\ @surround_separator)
when is_doc(left) and is_list(docs) and is_doc(right) and is_function(fun, 2) and is_doc(separator) do
@spec surround_many(binary, [any], binary, Inspect.Opts.t, (term, Inspect.Opts.t -> t), binary) :: t
def surround_many(left, docs, right, opts, fun, separator \\ @surround_separator) do
do_surround_many(left, docs, right, opts.limit, opts, fun, separator)
end
@@ -601,25 +516,16 @@ defmodule Inspect.Algebra do
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
@doc ~S"""
Formats a given document for a given width.
@doc """
The formatting function.
Takes the maximum width and a document to print as its arguments
and returns an IO data representation of the best layout for the
document to fit in the given width.
## Examples
iex> doc = Inspect.Algebra.glue("hello", " ", "world")
iex> Inspect.Algebra.format(doc, 30) |> IO.iodata_to_binary()
"hello world"
iex> Inspect.Algebra.format(doc, 10) |> IO.iodata_to_binary()
"hello\nworld"
"""
@spec format(t, non_neg_integer | :infinity) :: iodata
def format(doc, width) when is_doc(doc) and (width == :infinity or width >= 0) do
format(width, 0, [{0, default_mode(width), doc_group(doc)}])
def format(d, w) when w == :infinity or w >= 0 do
format(w, 0, [{0, default_mode(w), doc_group(d)}])
end
defp default_mode(:infinity), do: :flat
@@ -634,11 +540,10 @@ defmodule Inspect.Algebra do
defp fits?(_, [{_, _, :doc_line} | _]), do: true
defp fits?(w, [{_, _, :doc_nil} | t]), do: fits?(w, t)
defp fits?(w, [{i, m, doc_cons(x, y)} | t]), do: fits?(w, [{i, m, x} | [{i, m, y} | t]])
defp fits?(w, [{i, m, doc_color(x, _)} | t]), do: fits?(w, [{i, m, x} | t])
defp fits?(w, [{i, m, doc_nest(x, j)} | t]), do: fits?(w, [{i + j, m, x} | t])
defp fits?(w, [{i, _, doc_group(x)} | t]), do: fits?(w, [{i, :flat, x} | t])
defp fits?(w, [{_, _, s} | t]) when is_binary(s), do: fits?((w - byte_size(s)), t)
defp fits?(w, [{_, :flat, doc_break(s)} | t]), do: fits?((w - byte_size(s)), t)
defp fits?(w, [{_, _, s} | t]) when is_binary(s), do: fits?((w - byte_size s), t)
defp fits?(w, [{_, :flat, doc_break(s)} | t]), do: fits?((w - byte_size s), t)
defp fits?(_, [{_, :break, doc_break(_)} | _]), do: true
@spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
@@ -648,9 +553,8 @@ defmodule Inspect.Algebra do
defp format(w, k, [{i, m, doc_cons(x, y)} | t]), do: format(w, k, [{i, m, x} | [{i, m, y} | t]])
defp format(w, k, [{i, m, doc_nest(x, j)} | t]), do: format(w, k, [{i + j, m, x} | t])
defp format(w, k, [{i, m, doc_group(x)} | t]), do: format(w, k, [{i, m, x} | t])
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [ansi(c) | format(w, k, [{i, m, x} | t])]
defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, (k + byte_size(s)), t)]
defp format(w, k, [{_, :flat, doc_break(s)} | t]), do: [s | format(w, (k + byte_size(s)), t)]
defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, (k + byte_size s), t)]
defp format(w, k, [{_, :flat, doc_break(s)} | t]), do: [s | format(w, (k + byte_size s), t)]
defp format(w, k, [{i, :break, doc_break(s)} | t]) do
k = k + byte_size(s)
@@ -661,10 +565,6 @@ defmodule Inspect.Algebra do
end
end
defp ansi(color) do
IO.ANSI.format_fragment(color, true)
end
defp indent(0), do: @newline
defp indent(i), do: @newline <> :binary.copy(" ", i)
end
+75 -164
View File
@@ -60,97 +60,33 @@ defmodule Integer do
end
@doc """
Computes the modulo remainder of an integer division.
`Integer.mod/2` uses floored division, which means that
the result will always have the sign of the `divisor`.
Raises an `ArithmeticError` exception if one of the arguments is not an
integer, or when the `divisor` is `0`.
## Examples
iex> Integer.mod(5, 2)
1
iex> Integer.mod(6, -4)
-2
"""
@spec mod(integer, neg_integer | pos_integer) :: integer
def mod(dividend, divisor) do
remainder = rem(dividend, divisor)
if remainder * divisor < 0 do
remainder + divisor
else
remainder
end
end
@doc """
Performs a floored integer division.
Raises an `ArithmeticError` exception if one of the arguments is not an
integer, or when the `divisor` is `0`.
`Integer.floor_div/2` performs *floored* integer division. This means that
the result is always rounded towards negative infinity.
If you want to perform truncated integer division (rounding towards zero),
use `Kernel.div/2` instead.
## Examples
iex> Integer.floor_div(5, 2)
2
iex> Integer.floor_div(6, -4)
-2
iex> Integer.floor_div(-99, 2)
-50
"""
@spec floor_div(integer, neg_integer | pos_integer) :: integer
def floor_div(dividend, divisor) do
if (dividend * divisor < 0) and rem(dividend, divisor) != 0 do
div(dividend, divisor) - 1
else
div(dividend, divisor)
end
end
@doc """
Returns the ordered digits for the given `integer`.
Returns the ordered digits for the given non-negative `integer`.
An optional `base` value may be provided representing the radix for the returned
digits. This one must be an integer >= 2.
digits. This one can be an integer >= 2.
## Examples
iex> Integer.digits(123)
[1, 2, 3]
iex> Integer.digits(101)
[1, 0, 1]
iex> Integer.digits(170, 2)
[1, 0, 1, 0, 1, 0, 1, 0]
iex> Integer.digits(-170, 2)
[-1, 0, -1, 0, -1, 0, -1, 0]
"""
@spec digits(integer, pos_integer) :: [integer, ...]
@spec digits(non_neg_integer, pos_integer) :: [non_neg_integer, ...]
def digits(integer, base \\ 10)
when is_integer(integer) and is_integer(base) and base >= 2 do
when is_integer(integer) and integer >= 0 and is_integer(base) and base >= 2 do
do_digits(integer, base, [])
end
defp do_digits(digit, base, []) when abs(digit) < base,
do: [digit]
defp do_digits(digit, base, []) when digit == -base,
do: [-1, 0]
defp do_digits(base, base, []),
do: [1, 0]
defp do_digits(0, _base, acc),
do: acc
defp do_digits(integer, base, acc),
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
defp do_digits(0, _base, []), do: [0]
defp do_digits(1, _base, []), do: [1]
defp do_digits(base, base, []), do: [1, 0]
defp do_digits(0, _base, acc), do: acc
defp do_digits(integer, base, acc) do
do_digits div(integer, base), base, [rem(integer, base) | acc]
end
@doc """
Returns the integer represented by the ordered `digits`.
@@ -175,21 +111,14 @@ defmodule Integer do
do_undigits(digits, base, 0)
end
defp do_undigits([], _base, 0),
do: 0
defp do_undigits([digit], base, 0) when is_integer(digit) and digit < base,
do: digit
defp do_undigits([1, 0], base, 0),
do: base
defp do_undigits([0 | tail], base, 0),
do: do_undigits(tail, base, 0)
defp do_undigits([], _base, acc),
do: acc
defp do_undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise ArgumentError, "invalid digit #{digit} in base #{base}"
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
do: do_undigits(tail, base, acc * base + digit)
defp do_undigits([], _base, []), do: 0
defp do_undigits([0], _base, []), do: 0
defp do_undigits([1], _base, []), do: 1
defp do_undigits([1, 0], base, []), do: base
defp do_undigits([], _base, acc), do: acc
defp do_undigits([digit | tail], base, acc) do
do_undigits(tail, base, acc * base + digit)
end
@doc """
Parses a text representation of an integer.
@@ -232,46 +161,74 @@ defmodule Integer do
** (ArgumentError) invalid base 38
"""
@spec parse(binary, 2..36) :: {integer, binary} | :error
@spec parse(binary, 2..36) :: {integer, binary} | :error | no_return
def parse(binary, base \\ 10)
def parse(_binary, base) when not base in 2..36 do
raise ArgumentError, "invalid base #{inspect base}"
def parse("", base) when base in 2..36,
do: :error
def parse(binary, base) when is_binary(binary) and base in 2..36 do
parse_in_base(binary, base)
end
def parse(binary, base) do
case count_digits(binary, base) do
0 ->
:error
count ->
{digits, rem} = :erlang.split_binary(binary, count)
{:erlang.binary_to_integer(digits, base), rem}
def parse(binary, base) when is_binary(binary) do
raise ArgumentError, "invalid base #{base}"
end
defp parse_in_base("-" <> bin, base) do
case do_parse(bin, base) do
{number, remainder} -> {-number, remainder}
:error -> :error
end
end
defp count_digits(<<sign, rest::binary>>, base) when sign in '+-' do
case count_digits_nosign(rest, base, 1) do
1 -> 0
count -> count
defp parse_in_base("+" <> bin, base) do
do_parse(bin, base)
end
defp parse_in_base(binary, base) when is_binary(binary) do
do_parse(binary, base)
end
defp do_parse(<<char, rest::binary>>, base) do
if valid_digit_in_base?(char, base) do
do_parse(rest, base, parse_digit(char, base))
else
:error
end
end
defp count_digits(<<rest::binary>>, base) do
count_digits_nosign(rest, base, 0)
defp do_parse(_, _) do
:error
end
digits = [{?0..?9, -?0}, {?A..?Z, 10 - ?A}, {?a..?z, 10 - ?a}]
for {chars, diff} <- digits, char <- chars do
digit = char + diff
defp count_digits_nosign(<<unquote(char), rest::binary>>, base, count)
when base > unquote(digit) do
count_digits_nosign(rest, base, count + 1)
defp do_parse(<<char, rest::binary>> = bin, base, acc) do
if valid_digit_in_base?(char, base) do
do_parse(rest, base, base * acc + parse_digit(char, base))
else
{acc, bin}
end
end
defp count_digits_nosign(<<_::binary>>, _, count), do: count
defp do_parse(bitstring, _, acc) do
{acc, bitstring}
end
defp parse_digit(char, _) do
cond do
char in ?0..?9 -> char - ?0
char in ?A..?Z -> char - ?A + 10
true -> char - ?a + 10
end
end
defp valid_digit_in_base?(char, base) do
if base <= 10 do
char in ?0..(?0 + base - 1)
else
char in ?0..?9 or char in ?A..(?A + base - 11) or char in ?a..(?a + base - 11)
end
end
@doc """
Returns a binary which corresponds to the text representation
@@ -373,54 +330,8 @@ defmodule Integer do
:erlang.integer_to_list(integer, base)
end
@doc """
Returns the greatest common divisor of the two given integers.
The greatest common divisor (GCD) of `integer1` and `integer2` is the largest positive
integer that divides both `integer1` and `integer2` without leaving a remainder.
By convention, `gcd(0, 0)` returns `0`.
## Examples
iex> Integer.gcd(2, 3)
1
iex> Integer.gcd(8, 12)
4
iex> Integer.gcd(8, -12)
4
iex> Integer.gcd(10, 0)
10
iex> Integer.gcd(7, 7)
7
iex> Integer.gcd(0, 0)
0
"""
@spec gcd(0, 0) :: 0
@spec gcd(integer, integer) :: pos_integer
def gcd(integer1, integer2) when is_integer(integer1) and is_integer(integer2) do
gcd_positive(abs(integer1), abs(integer2))
end
defp gcd_positive(0, integer2), do: integer2
defp gcd_positive(integer1, 0), do: integer1
defp gcd_positive(integer1, integer2), do: gcd_positive(integer2, rem(integer1, integer2))
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by v1.5
@doc false
@spec to_char_list(integer) :: charlist
def to_char_list(integer), do: Integer.to_charlist(integer)
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
@doc false
@spec to_char_list(integer, 2..36) :: charlist
def to_char_list(integer, base), do: Integer.to_charlist(integer, base)
end
+25 -58
View File
@@ -3,7 +3,7 @@ defmodule IO do
Functions handling input/output (IO).
Many functions in this module expect an IO device as an argument.
An IO device must be a PID or an atom representing a process.
An IO device must be a pid or an atom representing a process.
For convenience, Elixir provides `:stdio` and `:stderr` as
shortcuts to Erlang's `:standard_io` and `:standard_error`.
@@ -17,7 +17,7 @@ defmodule IO do
## IO devices
An IO device may be an atom or a PID. In case it is an atom,
An IO device may be an atom or a pid. In case it is an atom,
the atom must be the name of a registered process. In addition,
Elixir provides two shortcuts:
@@ -38,6 +38,8 @@ defmodule IO do
@type nodata :: {:error, term} | :eof
@type chardata() :: :unicode.chardata()
import :erlang, only: [group_leader: 0]
defmacrop is_iodata(data) do
quote do
is_list(unquote(data)) or is_binary(unquote(data))
@@ -53,7 +55,7 @@ defmodule IO do
It returns:
* `data` - the output characters
* `data` - the input characters
* `:eof` - end of file was encountered
@@ -65,7 +67,7 @@ defmodule IO do
empty string in case the device has reached EOF.
"""
@spec read(device, :all | :line | non_neg_integer) :: chardata | nodata
def read(device \\ :stdio, line_or_chars)
def read(device \\ group_leader(), line_or_chars)
def read(device, :all) do
do_read_all(map_dev(device), "")
@@ -90,13 +92,13 @@ defmodule IO do
@doc """
Reads from the IO `device`. The operation is Unicode unsafe.
The `device` is iterated by the given number of bytes or line by line if
The `device` is iterated by the given number of characters or line by line if
`:line` is given.
Alternatively, if `:all` is given, then whole `device` is returned.
It returns:
* `data` - the output bytes
* `data` - the input characters
* `:eof` - end of file was encountered
@@ -111,7 +113,7 @@ defmodule IO do
as it will return the wrong result.
"""
@spec binread(device, :all | :line | non_neg_integer) :: iodata | nodata
def binread(device \\ :stdio, line_or_chars)
def binread(device \\ group_leader(), line_or_chars)
def binread(device, :all) do
do_binread_all(map_dev(device), "")
@@ -156,7 +158,7 @@ defmodule IO do
"""
@spec write(device, chardata | String.Chars.t) :: :ok
def write(device \\ :stdio, item) do
def write(device \\ group_leader(), item) do
:io.put_chars map_dev(device), to_chardata(item)
end
@@ -171,7 +173,7 @@ defmodule IO do
as it will return the wrong result.
"""
@spec binwrite(device, iodata) :: :ok | {:error, term}
def binwrite(device \\ :stdio, item) when is_iodata(item) do
def binwrite(device \\ group_leader(), item) when is_iodata(item) do
:file.write map_dev(device), item
end
@@ -180,7 +182,7 @@ defmodule IO do
but adds a newline at the end.
"""
@spec puts(device, chardata | String.Chars.t) :: :ok
def puts(device \\ :stdio, item) do
def puts(device \\ group_leader(), item) do
:io.put_chars map_dev(device), [to_chardata(item), ?\n]
end
@@ -231,67 +233,32 @@ defmodule IO do
@doc """
Inspects and writes the given `item` to the device.
It's important to note that it returns the given `item` unchanged.
This makes it possible to "spy" on values by inserting an
`IO.inspect/2` call almost anywhere in your code, for example,
in the middle of a pipeline.
It enables pretty printing by default with width of
80 characters. The width can be changed by explicitly
passing the `:width` option.
The output can be decorated with a label, by providing the `:label`
option to easily distinguish it from other `IO.inspect/2` calls.
The label will be printed before the inspected `item`.
See `Inspect.Opts` for a full list of remaining formatting options.
See `Inspect.Opts` for a full list of options.
## Examples
IO.inspect <<0, 1, 2>>, width: 40
Prints:
<<0, 1, 2>>
We can use the `:label` option to decorate the output:
IO.inspect 1..100, label: "a wonderful range"
Prints:
a wonderful range: 1..100
The `:label` option is especially useful with pipelines:
[1, 2, 3]
|> IO.inspect(label: "before")
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "after")
|> Enum.sum
Prints:
before: [1, 2, 3]
after: [2, 4, 6]
IO.inspect Process.list, width: 40
"""
@spec inspect(item, keyword) :: item when item: var
@spec inspect(item, Keyword.t) :: item when item: var
def inspect(item, opts \\ []) do
inspect :stdio, item, opts
inspect group_leader(), item, opts
end
@doc """
Inspects `item` according to the given options using the IO `device`.
See `inspect/2` for a full list of options.
See `Inspect.Opts` for a full list of options.
"""
@spec inspect(device, item, keyword) :: item when item: var
@spec inspect(device, item, Keyword.t) :: item when item: var
def inspect(device, item, opts) when is_list(opts) do
label = if (label = opts[:label]), do: [to_chardata(label), ": "], else: []
opts = struct(Inspect.Opts, opts)
chardata = Inspect.Algebra.format(Inspect.Algebra.to_doc(item, opts), opts.width)
puts device, [label, chardata]
opts = struct(Inspect.Opts, opts)
iodata = Inspect.Algebra.format(Inspect.Algebra.to_doc(item, opts), opts.width)
puts device, iodata
item
end
@@ -310,7 +277,7 @@ defmodule IO do
def getn(prompt, count \\ 1)
def getn(prompt, count) when is_integer(count) and count > 0 do
getn(:stdio, prompt, count)
getn(group_leader, prompt, count)
end
def getn(device, prompt) when not is_integer(prompt) do
@@ -340,7 +307,7 @@ defmodule IO do
:io.get_chars(map_dev(device), to_chardata(prompt), count)
end
@doc ~S"""
@doc """
Reads a line from the IO `device`.
It returns:
@@ -362,7 +329,7 @@ defmodule IO do
"""
@spec gets(device, chardata | String.Chars.t) :: chardata | nodata
def gets(device \\ :stdio, prompt) do
def gets(device \\ group_leader(), prompt) do
:io.get_line(map_dev(device), to_chardata(prompt))
end
@@ -376,7 +343,7 @@ defmodule IO do
The `device` is iterated by the given number of characters or line by line if
`:line` is given.
This reads from the IO as UTF-8. Check out
This reads from the IO as utf-8. Check out
`IO.binstream/2` to handle the IO as a raw binary.
Note that an IO stream has side effects and every time
+36 -48
View File
@@ -25,9 +25,9 @@ defmodule IO.ANSI do
import IO.ANSI.Sequence
@type ansicode :: atom
@type ansilist :: maybe_improper_list(char | ansicode | binary | ansilist, binary | ansicode | [])
@type ansidata :: ansilist | ansicode | binary
@typep ansicode :: atom
@typep ansilist :: maybe_improper_list(char | ansicode | binary | ansilist, binary | ansicode | [])
@type ansidata :: ansilist | ansicode | binary
@doc """
Checks if ANSI coloring is supported and enabled on this machine.
@@ -42,7 +42,7 @@ defmodule IO.ANSI do
Application.get_env(:elixir, :ansi_enabled, false)
end
@doc "Sets foreground color."
@doc "Sets foreground color"
@spec color(0..255) :: String.t
def color(code) when code in 0..255, do: "\e[38;5;#{code}m"
@@ -56,7 +56,7 @@ defmodule IO.ANSI do
color(16 + (36 * r) + (6 * g) + b)
end
@doc "Sets background color."
@doc "Sets background color"
@spec color_background(0..255) :: String.t
def color_background(code) when code in 0..255, do: "\e[48;5;#{code}m"
@@ -70,109 +70,97 @@ defmodule IO.ANSI do
color_background(16 + (36 * r) + (6 * g) + b)
end
@doc "Resets all attributes."
@doc "Resets all attributes"
defsequence :reset, 0
@doc "Bright (increased intensity) or bold."
@doc "Bright (increased intensity) or Bold"
defsequence :bright, 1
@doc "Faint (decreased intensity). Not widely supported."
@doc "Faint (decreased intensity), not widely supported"
defsequence :faint, 2
@doc "Italic: on. Not widely supported. Sometimes treated as inverse."
@doc "Italic: on. Not widely supported. Sometimes treated as inverse"
defsequence :italic, 3
@doc "Underline: single."
@doc "Underline: Single"
defsequence :underline, 4
@doc "Blink: slow. Less than 150 per minute."
@doc "Blink: Slow. Less than 150 per minute"
defsequence :blink_slow, 5
@doc "Blink: rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported."
@doc "Blink: Rapid. MS-DOS ANSI.SYS; 150 per minute or more; not widely supported"
defsequence :blink_rapid, 6
@doc "Image: negative. Swap foreground and background."
@doc "Image: Negative. Swap foreground and background"
defsequence :inverse, 7
@doc "Image: negative. Swap foreground and background."
@doc "Image: Negative. Swap foreground and background"
defsequence :reverse, 7
@doc "Conceal. Not widely supported."
@doc "Conceal. Not widely supported"
defsequence :conceal, 8
@doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported."
@doc "Crossed-out. Characters legible, but marked for deletion. Not widely supported"
defsequence :crossed_out, 9
@doc "Sets primary (default) font."
@doc "Sets primary (default) font"
defsequence :primary_font, 10
for font_n <- [1, 2, 3, 4, 5, 6, 7, 8, 9] do
@doc "Sets alternative font #{font_n}."
@doc "Sets alternative font #{font_n}"
defsequence :"font_#{font_n}", font_n + 10
end
@doc "Normal color or intensity."
@doc "Normal color or intensity"
defsequence :normal, 22
@doc "Not italic."
@doc "Not italic"
defsequence :not_italic, 23
@doc "Underline: none."
@doc "Underline: None"
defsequence :no_underline, 24
@doc "Blink: off."
@doc "Blink: off"
defsequence :blink_off, 25
@doc "Image: positive. Normal foreground and background."
defsequence :inverse_off, 27
@doc "Image: positive. Normal foreground and background."
defsequence :reverse_off, 27
colors = [:black, :red, :green, :yellow, :blue, :magenta, :cyan, :white]
for {color, code} <- Enum.with_index(colors) do
@doc "Sets foreground color to #{color}."
@doc "Sets foreground color to #{color}"
defsequence color, code + 30
@doc "Sets foreground color to light #{color}."
defsequence :"light_#{color}", code + 90
@doc "Sets background color to #{color}."
@doc "Sets background color to #{color}"
defsequence :"#{color}_background", code + 40
@doc "Sets background color to light #{color}."
defsequence :"light_#{color}_background", code + 100
end
@doc "Default text color."
@doc "Default text color"
defsequence :default_color, 39
@doc "Default background color."
@doc "Default background color"
defsequence :default_background, 49
@doc "Framed."
@doc "Framed"
defsequence :framed, 51
@doc "Encircled."
@doc "Encircled"
defsequence :encircled, 52
@doc "Overlined."
@doc "Overlined"
defsequence :overlined, 53
@doc "Not framed or encircled."
@doc "Not framed or encircled"
defsequence :not_framed_encircled, 54
@doc "Not overlined."
@doc "Not overlined"
defsequence :not_overlined, 55
@doc "Sends cursor home."
@doc "Sends cursor home"
defsequence :home, "", "H"
@doc "Clears screen."
@doc "Clears screen"
defsequence :clear, "2", "J"
@doc "Clears line."
@doc "Clears line"
defsequence :clear_line, "2", "K"
defp format_sequence(other) do
@@ -198,7 +186,7 @@ defmodule IO.ANSI do
[[[[[[], "Hello, "] | "\e[31m"] | "\e[1m"], "world!"] | "\e[0m"]
"""
def format(chardata, emit? \\ enabled?()) when is_boolean(emit?) do
def format(chardata, emit? \\ enabled?) when is_boolean(emit?) do
do_format(chardata, [], [], emit?, :maybe)
end
@@ -218,7 +206,7 @@ defmodule IO.ANSI do
[[[[[[] | "\e[1m"], 87], 111], 114], 100]
"""
def format_fragment(chardata, emit? \\ enabled?()) when is_boolean(emit?) do
def format_fragment(chardata, emit? \\ enabled?) when is_boolean(emit?) do
do_format(chardata, [], [], emit?, false)
end
+58 -95
View File
@@ -2,7 +2,6 @@ defmodule IO.ANSI.Docs do
@moduledoc false
@bullets [?*, ?-, ?+]
@spaces [" ", "\n", "\t"]
@doc """
The default options used by this module.
@@ -11,11 +10,11 @@ defmodule IO.ANSI.Docs do
* `:enabled` - toggles coloring on and off (true)
* `:doc_bold` - bold text (bright)
* `:doc_code` - code blocks (cyan)
* `:doc_headings` - h1, h2, h3, h4, h5, h6 headings (yellow)
* `:doc_code` - code blocks (cyan, bright)
* `:doc_headings` - h1 and h2 headings (yellow, bright)
* `:doc_inline_code` - inline code (cyan)
* `:doc_table_heading` - style for table headings
* `:doc_title` - top level heading (reverse, yellow)
* `:doc_title` - top level heading (reverse, yellow, bright)
* `:doc_underline` - underlined text (underline)
* `:width` - the width to format the text (80)
@@ -25,7 +24,7 @@ defmodule IO.ANSI.Docs do
def default_options do
[enabled: true,
doc_bold: [:bright],
doc_code: [:cyan],
doc_code: [:cyan, :bright],
doc_headings: [:yellow],
doc_inline_code: [:cyan],
doc_table_heading: [:reverse],
@@ -41,12 +40,12 @@ defmodule IO.ANSI.Docs do
"""
def print_heading(heading, options \\ []) do
IO.puts IO.ANSI.reset
options = Keyword.merge(default_options(), options)
options = Keyword.merge(default_options, options)
width = options[:width]
padding = div(width + String.length(heading), 2)
heading = heading |> String.pad_leading(padding) |> String.pad_trailing(width)
write(:doc_title, heading, options)
newline_after_block()
newline_after_block
end
@doc """
@@ -56,7 +55,7 @@ defmodule IO.ANSI.Docs do
defined in `default_options/1`.
"""
def print(doc, options \\ []) do
options = Keyword.merge(default_options(), options)
options = Keyword.merge(default_options, options)
doc
|> String.split(["\r\n", "\n"], trim: false)
|> Enum.map(&String.trim_trailing/1)
@@ -67,23 +66,22 @@ defmodule IO.ANSI.Docs do
write_text(text, indent, options)
end
defp process(["# " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
defp process(["# " <> heading | rest], text, indent, options) do
write_text(text, indent, options)
write_h1(String.trim(heading), options)
process(rest, [], "", options)
end
defp process(["## " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
defp process(["## " <> heading | rest], text, indent, options) do
write_text(text, indent, options)
write_h2(String.trim(heading), options)
process(rest, [], "", options)
end
defp process(["### " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process(["#### " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process(["##### " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
end
defp process(["###### " <> _ = heading | rest], text, indent, options) do
write_heading(heading, rest, text, indent, options)
defp process(["### " <> heading | rest], text, indent, options) do
write_text(text, indent, options)
write_h3(String.trim(heading), indent, options)
process(rest, [], "", options)
end
defp process(["" | rest], text, indent, options) do
@@ -104,7 +102,7 @@ defmodule IO.ANSI.Docs do
process_fenced_code_block(rest, text, indent, options, _delimiter = "~~~")
end
defp process(all = [line | rest], text, indent, options) do
defp process(all=[line | rest], text, indent, options) do
{stripped, count} = strip_spaces(line, 0, :infinity)
cond do
link_label?(stripped, count) ->
@@ -120,11 +118,19 @@ defmodule IO.ANSI.Docs do
## Headings
defp write_heading(heading, rest, text, indent, options) do
write_text(text, indent, options)
defp write_h1(heading, options) do
write_h2(String.upcase(heading), options)
end
defp write_h2(heading, options) do
write(:doc_headings, heading, options)
newline_after_block()
process(rest, [], "", options)
newline_after_block
end
defp write_h3(heading, indent, options) do
IO.write(indent)
write(:doc_headings, heading, options)
newline_after_block
end
## Lists
@@ -205,7 +211,7 @@ defmodule IO.ANSI.Docs do
|> Enum.join(" ")
|> handle_links
|> handle_inline(options)
|> String.split(@spaces)
|> String.split(~r{\s})
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap)
unless no_wrap, do: newline_after_block()
@@ -249,8 +255,8 @@ defmodule IO.ANSI.Docs do
end
defp write_code(code, indent, options) do
write(:doc_code, "#{indent} #{Enum.join(Enum.reverse(code), "\n#{indent} ")}", options)
newline_after_block()
write(:doc_code, "#{indent}┃ #{Enum.join(Enum.reverse(code), "\n#{indent}┃ ")}", options)
newline_after_block
end
## Tables
@@ -258,7 +264,7 @@ defmodule IO.ANSI.Docs do
defp process_table(lines, indent, options) do
{table, rest} = Enum.split_while(lines, &table_line?/1)
table_lines(table, options)
newline_after_block()
newline_after_block
process(rest, [], indent, options)
end
@@ -267,14 +273,8 @@ defmodule IO.ANSI.Docs do
count = Enum.map(lines, &length/1) |> Enum.max
lines = Enum.map(lines, &pad_to_number_of_columns(&1, count))
widths =
for line <- lines do
if table_header?(line) do
for _ <- line, do: 0
else
for {_col, length} <- line, do: length
end
end
widths = for line <- lines, do:
(for {_col, length} <- line, do: length)
col_widths = Enum.reduce(widths,
List.duplicate(0, count),
@@ -287,17 +287,15 @@ defmodule IO.ANSI.Docs do
line
|> String.trim("|")
|> String.trim()
|> String.split(" | ")
|> String.split(~r/\s\|\s/)
|> Enum.map(&render_column(&1, options))
end
defp render_column(col, options) do
col =
col
|> String.replace("\\\|", "|")
|> String.trim()
|> handle_links
|> handle_inline(options)
col = col
|> String.replace(~r/\\ \|/x, "|")
|> handle_links
|> handle_inline(options)
{col, length_without_escape(col, 0)}
end
@@ -311,8 +309,6 @@ defmodule IO.ANSI.Docs do
defp render_table([first, second | rest], widths, options) do
combined = Enum.zip(first, widths)
if table_header?(second) do
alignments = Enum.map(second, &column_alignment/1)
options = Keyword.put_new(options, :alignments, alignments)
draw_table_row(combined, options, :heading)
render_table(rest, widths, options)
else
@@ -330,34 +326,14 @@ defmodule IO.ANSI.Docs do
defp render_table([], _, _),
do: nil
defp column_alignment({line, _}) do
cond do
String.starts_with?(line, ":") and String.ends_with?(line, ":") -> :center
String.ends_with?(line, ":") -> :right
true -> :left
end
end
defp table_header?(row) do
Enum.all?(row, fn {col, _} -> table_header_column?(col) end)
end
defp table_header_column?(":" <> row), do: table_header_contents?(row)
defp table_header_column?(row), do: table_header_contents?(row)
defp table_header_contents?("-" <> row), do: table_header_contents?(row)
defp table_header_contents?(":"), do: true
defp table_header_contents?(""), do: true
defp table_header_contents?(_), do: false
defp table_header?(row), do:
Enum.all?(row, fn {col, _} -> col =~ ~r/^:?-+:?$/ end)
defp draw_table_row(cols_and_widths, options, heading \\ false) do
default_alignments = List.duplicate(:left, length(cols_and_widths))
alignments = Keyword.get(options, :alignments, default_alignments)
columns =
cols_and_widths
|> Enum.zip(alignments)
|> Enum.map_join(" | ", &generate_table_cell/1)
Enum.map_join(cols_and_widths, " | ", fn {{col, length}, width} ->
col <> String.duplicate(" ", width - length)
end)
if heading do
write(:doc_table_heading, columns, options)
@@ -366,22 +342,12 @@ defmodule IO.ANSI.Docs do
end
end
defp generate_table_cell({{{col, length}, width}, :center}) do
pad = if rem(length, 2) == 0, do: 1, else: rem(width, 2)
spaces = div(width, 2) - div(length, 2)
String.duplicate(" ", spaces) <> col <> String.duplicate(" ", spaces + pad)
end
defp generate_table_cell({{{col, length}, width}, :right}) do
String.duplicate(" ", width - length) <> col
end
defp generate_table_cell({{{col, length}, width}, _}) do
col <> String.duplicate(" ", width - length)
end
defp table_line?(line) do
line =~ " | "
Regex.match?(~r'''
( ^ \s{0,3} \| (?: [^|]+ \|)+ \s* $ )
|
(\s \| \s)
'''x, line)
end
## Helpers
@@ -457,17 +423,14 @@ defmodule IO.ANSI.Docs do
end
defp escape_underlines_in_link(text) do
~r{https?\S*}
|> Regex.recompile!
|> Regex.replace(text, &String.replace(&1, "_", "\\_"))
Regex.replace(~r{https?\S*}, text, &String.replace(&1, "_", "\\_"))
end
defp remove_square_brackets_in_link(text) do
~r{\[(.*?)\]\((.*?)\)}
|> Regex.recompile!
|> Regex.replace(text, "\\1 (\\2)")
Regex.replace(~r{\[(.*?)\]\((.*?)\)}, text, "\\1 (\\2)")
end
# We have four entries: **, *, _ and `.
#
# The first three behave the same while the last one is simpler
+448 -789
View File
File diff suppressed because it is too large Load Diff
+26 -55
View File
@@ -59,20 +59,6 @@ defmodule Kernel.CLI do
Enum.reverse(config.errors, errors)
end
@doc false
def format_error(kind, reason, stacktrace) do
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
iodata =
case blamed do
%FunctionClauseError{} ->
[Exception.format_banner(kind, reason, stacktrace),
pad(FunctionClauseError.blame(blamed, &inspect/1, &blame_match/2))]
_ ->
Exception.format_banner(kind, blamed, stacktrace)
end
[iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
end
## Helpers
defp at_exit(res) do
@@ -90,22 +76,22 @@ defmodule Kernel.CLI do
fun.(elem(res, 1))
catch
:exit, {:shutdown, int} when is_integer(int) ->
send parent, {self(), {:shutdown, int}}
send parent, {self, {:shutdown, int}}
exit({:shutdown, int})
:exit, reason
when reason == :normal
when reason == :shutdown
when tuple_size(reason) == 2 and elem(reason, 0) == :shutdown ->
send parent, {self(), {:shutdown, 0}}
send parent, {self, {:shutdown, 0}}
exit(reason)
kind, reason ->
stack = System.stacktrace
print_error(kind, reason, stack)
send parent, {self(), {:shutdown, 1}}
send parent, {self, {:shutdown, 1}}
exit(to_exit(kind, reason, stack))
else
_ ->
send parent, {self(), res}
send parent, {self, res}
end
end)
@@ -133,36 +119,13 @@ defmodule Kernel.CLI do
end
end
## Error handling
defp print_error(kind, reason, stacktrace) do
IO.write :stderr, format_error(kind, reason, stacktrace)
defp print_error(kind, reason, trace) do
IO.puts :stderr, Exception.format(kind, reason, prune_stacktrace(trace))
end
defp blame_match(%{match?: true, node: node}, _),
do: blame_ansi(:normal, "+", node)
defp blame_match(%{match?: false, node: node}, _),
do: blame_ansi(:red, "-", node)
defp blame_match(_, string),
do: string
defp blame_ansi(color, no_ansi, node) do
if IO.ANSI.enabled? do
[color | Macro.to_string(node)]
|> IO.ANSI.format(true)
|> IO.iodata_to_binary()
else
no_ansi <> Macro.to_string(node) <> no_ansi
end
end
defp pad(string) do
" " <> String.replace(string, "\n", "\n ")
end
@elixir_internals [:elixir, :elixir_expand, :elixir_compiler, :elixir_module,
:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map,
:elixir_erl, :elixir_erl_clauses, :elixir_erl_pass, Kernel.ErrorHandler]
@elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_clauses,
:elixir_translator, :elixir_expand, :elixir_lexical, :elixir_exp_clauses,
:elixir_def, :elixir_map]
defp prune_stacktrace([{mod, _, _, _} | t]) when mod in @elixir_internals do
prune_stacktrace(t)
@@ -448,17 +411,25 @@ defmodule Kernel.CLI do
end
defp filter_multiple_patterns(patterns) do
{files, missing} =
Enum.reduce patterns, {[], []}, fn pattern, {files, missing} ->
case filter_patterns(pattern) do
[] -> {files, [pattern | missing]}
match -> {match ++ files, missing}
end
matched_files = Enum.map patterns, fn(pattern) ->
case filter_patterns(pattern) do
[] -> {:missing, pattern}
files -> {:ok, files}
end
end
case missing do
[] -> {:ok, :lists.usort(files)}
_ -> {:missing, :lists.usort(missing)}
files = Enum.filter_map matched_files,
fn(match) -> elem(match, 0) == :ok end,
&elem(&1, 1)
missing_patterns = Enum.filter_map matched_files,
fn(match) -> elem(match, 0) == :missing end,
&elem(&1, 1)
if missing_patterns == [] do
{:ok, :lists.usort(Enum.concat(files))}
else
{:missing, :lists.usort(missing_patterns)}
end
end
+1 -6
View File
@@ -3,19 +3,16 @@
defmodule Kernel.ErrorHandler do
@moduledoc false
@spec undefined_function(module, atom, list) :: term
def undefined_function(module, fun, args) do
ensure_loaded(module) or ensure_compiled(module, :module)
:error_handler.undefined_function(module, fun, args)
end
@spec undefined_lambda(module, fun, list) :: term
def undefined_lambda(module, fun, args) do
ensure_loaded(module) or ensure_compiled(module, :module)
:error_handler.undefined_lambda(module, fun, args)
end
@spec ensure_loaded(module) :: boolean
def ensure_loaded(module) do
case :code.ensure_loaded(module) do
{:module, _} -> true
@@ -23,17 +20,15 @@ defmodule Kernel.ErrorHandler do
end
end
@spec ensure_compiled(module, atom) :: boolean
# Never wait on nil because it should never be defined.
def ensure_compiled(nil, _kind) do
false
end
def ensure_compiled(module, kind) do
parent = :erlang.get(:elixir_compiler_pid)
ref = :erlang.make_ref
send parent, {:waiting, kind, self(), ref, module, :elixir_module.compiler_modules()}
:erlang.garbage_collect(self())
:erlang.garbage_collect(self)
receive do
{^ref, :found} -> true
{^ref, :not_found} -> false
+11 -32
View File
@@ -1,6 +1,5 @@
# This is an Elixir module responsible for tracking references
# to modules, remote dispatches, and the usage of
# aliases/imports/requires in the Elixir scope.
# This is an Elixir module responsible for tracking
# the usage of aliases, imports and requires in the Elixir scope.
#
# Note that since this is required for bootstrap, we can't use
# any of the `GenServer.Behaviour` conveniences.
@@ -40,7 +39,7 @@ defmodule Kernel.LexicalTracker do
# Internal API
# Starts the tracker and returns its PID.
# Starts the tracker and returns its pid.
@doc false
def start_link(dest) do
:gen_server.start_link(__MODULE__, dest, [])
@@ -52,47 +51,35 @@ defmodule Kernel.LexicalTracker do
end
@doc false
def add_import(pid, module, fas, line, warn) when is_atom(module) do
def add_import(pid, module, fas, line, warn) do
:gen_server.cast(pid, {:add_import, module, fas, line, warn})
end
@doc false
def add_alias(pid, module, line, warn) when is_atom(module) do
def add_alias(pid, module, line, warn) do
:gen_server.cast(pid, {:add_alias, module, line, warn})
end
@doc false
def remote_reference(pid, module, mode) when is_atom(module) do
def remote_reference(pid, module, mode) do
:gen_server.cast(pid, {:remote_reference, module, mode})
end
@doc false
def remote_dispatch(pid, module, fa, line, mode) when is_atom(module) do
def remote_dispatch(pid, module, fa, line, mode) do
:gen_server.cast(pid, {:remote_dispatch, module, fa, line, mode})
end
@doc false
def import_dispatch(pid, module, fa, line, mode) when is_atom(module) do
def import_dispatch(pid, module, fa, line, mode) do
:gen_server.cast(pid, {:import_dispatch, module, fa, line, mode})
end
@doc false
def alias_dispatch(pid, module) when is_atom(module) do
def alias_dispatch(pid, module) do
:gen_server.cast(pid, {:alias_dispatch, module})
end
@doc false
def write_cache(pid, value) do
key = :erlang.unique_integer()
:gen_server.cast(pid, {:write_cache, key, value})
key
end
@doc false
def read_cache(pid, key) do
:gen_server.call(pid, {:read_cache, key}, @timeout)
end
@doc false
def collect_unused_imports(pid) do
unused(pid, :import)
@@ -111,7 +98,7 @@ defmodule Kernel.LexicalTracker do
def init(dest) do
{:ok, %{directives: %{}, references: %{}, compile: %{},
runtime: %{}, dest: dest, cache: %{}}}
runtime: %{}, dest: dest}}
end
@doc false
@@ -136,14 +123,6 @@ defmodule Kernel.LexicalTracker do
{:reply, state.dest, state}
end
def handle_call({:read_cache, key}, _from, %{cache: cache} = state) do
{:reply, Map.fetch!(cache, key), state}
end
def handle_cast({:write_cache, key, value}, %{cache: cache} = state) do
{:noreply, Map.put(state, :cache, Map.put(cache, key, value))}
end
def handle_cast({:remote_reference, module, mode}, state) do
{:noreply, %{state | references: add_reference(state.references, module, mode)}}
end
@@ -167,7 +146,7 @@ defmodule Kernel.LexicalTracker do
{:noreply, %{state | directives: add_dispatch(state.directives, module, :alias)}}
end
def handle_cast({:add_import, module, fas, line, warn}, state) do
def handle_cast({:add_import, module, fas, line, warn}, state) when is_atom(module) do
directives =
state.directives
|> Enum.reject(&match?({{:import, {^module, _, _}}, _}, &1))
+73 -74
View File
@@ -30,11 +30,8 @@ defmodule Kernel.ParallelCompiler do
* `:each_module` - for each module compiled, invokes the callback passing
the file, module and the module bytecode
* `:each_warning` - for each warning, invokes the callback passing
the file, line number, and warning message
* `:dest` - the destination directory for the BEAM files. When using `files/2`,
this information is only used to properly annotate the BEAM files before
* `:dest` - the destination directory for the beam files. When using `files/2`,
this information is only used to properly annotate the beam files before
they are loaded into memory. If you want a file to actually be written to
`dest`, use `files_to_path/3` instead.
@@ -113,21 +110,17 @@ defmodule Kernel.ParallelCompiler do
:erlang.put(:elixir_compiler_file, file)
:erlang.process_flag(:error_handler, Kernel.ErrorHandler)
result =
try do
_ = if output do
:elixir_compiler.file_to_path(file, output)
else
:elixir_compiler.file(file, Keyword.get(options, :dest))
end
:ok
catch
kind, reason ->
{kind, reason, System.stacktrace}
exit(try do
_ = if output do
:elixir_compiler.file_to_path(file, output)
else
:elixir_compiler.file(file, Keyword.get(options, :dest))
end
send(parent, {:file_compiled, self(), file, result})
exit(:shutdown)
{:shutdown, file}
catch
kind, reason ->
{:failure, kind, reason, System.stacktrace}
end)
end
timeout = Keyword.get(options, :long_compilation_threshold, 10) * 1_000
@@ -152,18 +145,21 @@ defmodule Kernel.ParallelCompiler do
# Instead of releasing all files at once, we release them in groups
# based on the module they are waiting on. We pick the module being
# depended on with less edges, as it is the mostly likely source of
# error (for example, someone made a typo). This may not always be
# error (for example, someone made a type). This may not always be
# true though: for example, if there is a macro injecting code into
# multiple modules and such code becomes faulty, now multiple modules
# are waiting on the same module required by the faulty code. However,
# since we need to pick something to be first, the one with fewer edges
# sounds like a sane choice.
entries
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
|> Enum.sort_by(&length(elem(&1, 1)))
|> case do
[{_on, refs} | _] -> spawn_compilers(%{state | entries: refs})
entries =
entries
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
|> Enum.sort_by(&length(elem(&1, 1)))
|> Enum.find_value([], &elem(&1, 1))
case entries do
[] -> handle_deadlock(waiting, queued)
_ -> spawn_compilers(%{state | entries: entries})
end
end
@@ -211,8 +207,8 @@ defmodule Kernel.ParallelCompiler do
{:waiting, kind, child, ref, on, defining} ->
# Oops, we already got it, do not put it on waiting.
# Alternatively, we're waiting on ourselves,
# send :found so that we can crash with a better error.
# OR
# We're waiting on ourselves, send :found so that we can crash with a better error
waiting =
if :lists.any(&match?({^kind, ^on}, &1), result) or on in defining do
send child, {ref, :found}
@@ -233,58 +229,26 @@ defmodule Kernel.ParallelCompiler do
end
spawn_compilers(state)
{:warning, file, line, message} ->
if callback = Keyword.get(options, :each_warning) do
callback.(file, line, message)
end
wait_for_messages(state)
{:file_compiled, child_pid, file, :ok} ->
discard_down(child_pid)
{:DOWN, _down_ref, :process, down_pid, {:shutdown, file}} ->
if callback = Keyword.get(options, :each_file) do
callback.(file)
end
cancel_waiting_timer(queued, child_pid)
cancel_waiting_timer(queued, down_pid)
# Sometimes we may have spurious entries in the waiting
# list because someone invoked try/rescue UndefinedFunctionError
new_entries = List.delete(entries, child_pid)
new_queued = List.keydelete(queued, child_pid, 0)
new_waiting = List.keydelete(waiting, child_pid, 1)
new_entries = List.delete(entries, down_pid)
new_queued = List.keydelete(queued, down_pid, 0)
new_waiting = List.keydelete(waiting, down_pid, 1)
spawn_compilers(%{state | entries: new_entries, waiting: new_waiting, queued: new_queued})
{:file_compiled, child_pid, file, {kind, reason, stack}} ->
discard_down(child_pid)
print_error(file, kind, reason, stack)
terminate(queued)
{:DOWN, ref, :process, _pid, reason} ->
handle_down(queued, ref, reason)
{:DOWN, down_ref, :process, _down_pid, reason} ->
handle_failure(down_ref, reason, queued)
wait_for_messages(state)
end
end
defp discard_down(pid) do
receive do
{:DOWN, _, :process, ^pid, _} -> :ok
end
end
defp handle_down(_queued, _ref, :normal) do
:ok
end
defp handle_down(queued, ref, reason) do
case List.keyfind(queued, ref, 1) do
{_child, ^ref, file, _timer_ref} ->
print_error(file, :exit, reason, [])
terminate(queued)
_ ->
:ok
end
end
defp handle_deadlock(waiting, queued) do
deadlock =
for {pid, _, file, _} <- queued do
@@ -294,7 +258,7 @@ defmodule Kernel.ParallelCompiler do
{_kind, ^pid, _, on, _} = List.keyfind(waiting, pid, 1)
error = CompileError.exception(description: "deadlocked waiting on module #{inspect on}",
file: nil, line: nil)
print_error(file, :error, error, stacktrace)
print_failure(file, {:failure, :error, error, stacktrace})
{file, on}
end
@@ -318,16 +282,51 @@ defmodule Kernel.ParallelCompiler do
exit({:shutdown, 1})
end
defp terminate(queued) do
for {pid, _, _, _} <- queued do
Process.exit(pid, :kill)
defp handle_failure(ref, reason, queued) do
if file = find_failure(ref, queued) do
print_failure(file, reason)
for {pid, _, _, _} <- queued do
Process.exit(pid, :kill)
end
exit({:shutdown, 1})
end
exit({:shutdown, 1})
end
defp print_error(file, kind, reason, stack) do
IO.write ["\n== Compilation error in file #{Path.relative_to_cwd(file)} ==\n",
Kernel.CLI.format_error(kind, reason, stack)]
defp find_failure(ref, queued) do
case List.keyfind(queued, ref, 1) do
{_child, ^ref, file, _timer_ref} -> file
_ -> nil
end
end
defp print_failure(_file, {:shutdown, _}) do
:ok
end
defp print_failure(file, {:failure, kind, reason, stacktrace}) do
IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
IO.puts Exception.format(kind, reason, prune_stacktrace(stacktrace))
end
defp print_failure(file, reason) do
IO.puts "\n== Compilation error on file #{Path.relative_to_cwd(file)} =="
IO.puts Exception.format(:exit, reason, [])
end
@elixir_internals [:elixir, :elixir_exp, :elixir_compiler, :elixir_module, :elixir_clauses,
:elixir_translator, :elixir_expand, :elixir_lexical, :elixir_exp_clauses,
:elixir_def, :elixir_map, Kernel.ErrorHandler]
defp prune_stacktrace([{mod, _, _, _} | t]) when mod in @elixir_internals do
prune_stacktrace(t)
end
defp prune_stacktrace([h | t]) do
[h | prune_stacktrace(t)]
end
defp prune_stacktrace([]) do
[]
end
defp cancel_waiting_timer(queued, child_pid) do
+28 -39
View File
@@ -49,22 +49,17 @@ defmodule Kernel.ParallelRequire do
defp spawn_requires([file | files], waiting, callbacks, schedulers, result) do
parent = self()
{pid, ref} = :erlang.spawn_monitor fn ->
:erlang.put(:elixir_compiler_pid, parent)
:erlang.put(:elixir_compiler_file, file)
result =
try do
new = Code.require_file(file) || []
{:required, Enum.map(new, &elem(&1, 0))}
catch
kind, reason ->
{kind, reason, System.stacktrace}
end
send(parent, {:file_required, self(), file, result})
exit(:shutdown)
exit(try do
new = Code.require_file(file) || []
{:required, Enum.map(new, &elem(&1, 0)), file}
catch
kind, reason ->
{:failure, kind, reason, System.stacktrace}
end)
end
spawn_requires(files, [{pid, ref} | waiting], callbacks, schedulers, result)
@@ -72,21 +67,28 @@ defmodule Kernel.ParallelRequire do
defp wait_for_messages(files, waiting, callbacks, schedulers, result) do
receive do
{:file_required, pid, file, {:required, mods}} ->
discard_down(pid)
if each_file_callback = callbacks[:each_file] do
each_file_callback.(file)
{:DOWN, ref, :process, pid, status} ->
tuple = {pid, ref}
if tuple in waiting do
waiting = List.delete(waiting, tuple)
case status do
{:required, mods, file} ->
if each_file_callback = callbacks[:each_file] do
each_file_callback.(file)
end
spawn_requires(files, waiting, callbacks, schedulers, mods ++ result)
{:failure, kind, reason, stacktrace} ->
:erlang.raise(kind, reason, stacktrace)
other ->
:erlang.raise(:exit, other, [])
end
else
spawn_requires(files, waiting, callbacks, schedulers, result)
end
waiting = List.keydelete(waiting, pid, 0)
spawn_requires(files, waiting, callbacks, schedulers, mods ++ result)
{:file_required, pid, _file, {kind, reason, stacktrace}} ->
discard_down(pid)
:erlang.raise(kind, reason, stacktrace)
{:DOWN, ref, :process, pid, reason} ->
handle_down(waiting, pid, ref, reason)
spawn_requires(files, waiting, callbacks, schedulers, result)
{:module_available, child, ref, file, module, binary} ->
if each_module_callback = callbacks[:each_module] do
@@ -104,17 +106,4 @@ defmodule Kernel.ParallelRequire do
spawn_requires(files, waiting, callbacks, schedulers, result)
end
end
defp discard_down(pid) do
receive do
{:DOWN, _, :process, ^pid, _} -> :ok
end
end
defp handle_down(waiting, pid, ref, reason) do
if reason != :normal and {pid, ref} in waiting do
:erlang.raise(:exit, reason, [])
end
:ok
end
end
+128 -159
View File
@@ -7,11 +7,13 @@ defmodule Kernel.SpecialForms do
`alias/2`, `case/2`, etc). The macros `{}` and `<<>>` are also special
forms used to define tuple and binary data structures respectively.
This module also documents macros that return information about Elixir's
compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0` and `__CALLER__/0`).
This module also documents Elixir's pseudo variables (`__ENV__`,
`__MODULE__`, `__DIR__` and `__CALLER__`). Pseudo variables return
information about Elixir's compilation environment and can only
be read, never assigned to.
Finally, it also documents two special forms, `__block__/1` and
`__aliases__/1`, which are not intended to be called directly by the
Finally, it also documents two special forms, `__block__` and
`__aliases__`, which are not intended to be called directly by the
developer but they appear in quoted contents since they are essential
in Elixir's constructs.
"""
@@ -27,21 +29,19 @@ defmodule Kernel.SpecialForms do
@doc """
Creates a tuple.
More information about the tuple data type and about functions to manipulate
tuples can be found in the `Tuple` module; some functions for working with
tuples are also available in `Kernel` (such as `Kernel.elem/2` or
`Kernel.tuple_size/1`).
Only two item tuples are considered literals in Elixir.
Therefore all other tuples are represented in the AST
as a call to the special form `:{}`.
## AST representation
Conveniences for manipulating tuples can be found in the
`Tuple` module. Some functions for working with tuples are
also available in `Kernel`, namely `Kernel.elem/2`,
`Kernel.put_elem/3` and `Kernel.tuple_size/1`.
Only two-item tuples are considered literals in Elixir and return themselves
when quoted. Therefore, all other tuples are represented in the AST as calls to
the `:{}` special form.
## Examples
iex> quote do
...> {1, 2}
...> end
{1, 2}
iex> {1, 2, 3}
{1, 2, 3}
iex> quote do
...> {1, 2, 3}
@@ -54,14 +54,56 @@ defmodule Kernel.SpecialForms do
@doc """
Creates a map.
See the `Map` module for more information about maps, their syntax, and ways to
access and manipulate them.
Maps are key-value stores where keys are compared
using the match operator (`===`). Maps can be created with
the `%{}` special form where keys are associated via `=>`:
%{1 => 2}
Maps also support the keyword notation, as other special forms,
as long as they are at the end of the argument list:
%{hello: :world, with: :keywords}
%{:hello => :world, with: :keywords}
If a map has duplicated keys, the last key will always have
higher precedence:
iex> %{a: :b, a: :c}
%{a: :c}
Conveniences for manipulating maps can be found in the
`Map` module.
## Access syntax
Besides the access functions available in the `Map` module,
like `Map.get/3` and `Map.fetch/2`, a map can be accessed using the
`.` operator:
iex> map = %{a: :b}
iex> map.a
:b
Note that the `.` operator expects the key `:a` to exist in the map.
If not, an `ArgumentError` is raised.
## Update syntax
Maps also support an update syntax:
iex> map = %{:a => :b}
iex> %{map | :a => :c}
%{:a => :c}
Notice the update syntax requires the given keys to exist.
Trying to update a key that does not exist will raise an `KeyError`.
## AST representation
Regardless of whether `=>` or the keyword syntax is used, key-value pairs in
maps are always represented internally as a list of two-element tuples for
simplicity:
Regardless if `=>` or the keywords syntax is used, Maps are
always represented internally as a list of two-element tuples
for simplicity:
iex> quote do
...> %{"a" => :b, c: :d}
@@ -131,10 +173,10 @@ defmodule Kernel.SpecialForms do
- `integer`
- `float`
- `bits` (alias for `bitstring`)
- `bits` (alias for bitstring)
- `bitstring`
- `binary`
- `bytes` (alias for `binary`)
- `bytes` (alias for binary)
- `utf8`
- `utf16`
- `utf32`
@@ -156,13 +198,13 @@ defmodule Kernel.SpecialForms do
iex> <<102, rest>>
** (ArgumentError) argument error
We can solve this by explicitly tagging it as `binary`:
We can solve this by explicitly tagging it as a binary:
iex> rest = "oo"
iex> <<102, rest::binary>>
"foo"
The `utf8`, `utf16`, and `utf32` types are for Unicode codepoints. They
The utf8, utf16, and utf32 types are for Unicode codepoints. They
can also be applied to literal strings and charlists:
iex> <<"foo"::utf16>>
@@ -222,7 +264,7 @@ defmodule Kernel.SpecialForms do
iex> x = 1
iex> <<x::8>> == <<x::size(8)>>
true
iex> <<x::8*4>> == <<x::size(8)-unit(4)>>
iex> <<x::8 * 4>> == <<x::size(8)-unit(4)>>
true
This syntax reflects the fact the effective size is given by
@@ -237,8 +279,8 @@ defmodule Kernel.SpecialForms do
-------------------- | ----------------
`signed` | `integer`
`unsigned` (default) | `integer`
`little` | `integer`, `float`, `utf16`, `utf32`
`big` (default) | `integer`, `float`, `utf16`, `utf32`
`little` | `integer`, `utf16`, `utf32`
`big` (default) | `integer`, `utf16`, `utf32`
`native` | `integer`, `utf16`, `utf32`
### Sign
@@ -293,7 +335,7 @@ defmodule Kernel.SpecialForms do
defmodule ImageTyper
@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)>>
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
@jpg_signature <<255::size(8), 216::size(8)>>
def type(<<@png_signature, rest::binary>>), do: :png
@@ -316,7 +358,7 @@ defmodule Kernel.SpecialForms do
defmacro unquote(:<<>>)(args), do: error!([args])
@doc """
Defines a remote call, a call to an anonymous function, or an alias.
Defines a remote call or an alias.
The dot (`.`) in Elixir can be used for remote calls:
@@ -324,16 +366,8 @@ defmodule Kernel.SpecialForms do
"foo"
In this example above, we have used `.` to invoke `downcase` in the
`String` module, passing `"FOO"` as argument.
The dot may be used to invoke anonymous functions too:
iex> (fn(n) -> n end).(7)
7
in which case there is a function on the left hand side.
We can also use the dot for creating aliases:
`String` alias, passing "FOO" as argument. We can also use the dot
for creating aliases:
iex> Hello.World
Hello.World
@@ -363,18 +397,11 @@ defmodule Kernel.SpecialForms do
iex> Kernel.'+'(1, 2)
3
Note that `Kernel."FUNCTION_NAME"` will be treated as a remote call and not an alias.
Note that `Kernel."HELLO"` will be treated as a remote call and not an alias.
This choice was done so every time single- or double-quotes are used, we have
a remote call regardless of the quote contents. This decision is also reflected
in the quoted expressions discussed below.
When the dot is used to invoke an anonymous function there is only one
operand, but it is still written using a postfix notation:
iex> negate = fn(n) -> -n end
iex> negate.(7)
-7
## Quoted expression
When `.` is used, the quoted expression may take two distinct
@@ -393,9 +420,9 @@ defmodule Kernel.SpecialForms do
This tuple follows the general quoted expression structure in Elixir,
with the name as first argument, some keyword list as metadata as second,
and the list of arguments as third. In this case, the arguments are the
alias `String` and the atom `:downcase`. The second argument in a remote call
is **always** an atom regardless of the literal used in the call:
and the number of arguments as third. In this case, the arguments is the
alias `String` and the atom `:downcase`. The second argument is **always**
an atom:
iex> quote do
...> String."downcase"("FOO")
@@ -405,15 +432,6 @@ defmodule Kernel.SpecialForms do
The tuple containing `:.` is wrapped in another tuple, which actually
represents the function call, and has `"FOO"` as argument.
In the case of calls to anonymous functions, the inner tuple with the dot
special form has only one argument, reflecting the fact that the operator is
unary:
iex> quote do
...> negate.(0)
...> end
{{:., [], [{:negate, [], __MODULE__}]}, [], [0]}
When the right side is an alias (i.e. starts with uppercase), we get instead:
iex> quote do
@@ -421,7 +439,7 @@ defmodule Kernel.SpecialForms do
...> end
{:__aliases__, [alias: false], [:Hello, :World]}
We go into more details about aliases in the `__aliases__/1` special form
We go into more details about aliases in the `__aliases__` special form
documentation.
## Unquoting
@@ -434,7 +452,7 @@ defmodule Kernel.SpecialForms do
...> end
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
Similar to `Kernel."FUNCTION_NAME"`, `unquote(x)` will always generate a remote call,
Similar to `Kernel."HELLO"`, `unquote(x)` will always generate a remote call,
independent of the value of `x`. To generate an alias via the quoted expression,
one needs to rely on `Module.concat/2`:
@@ -478,16 +496,6 @@ defmodule Kernel.SpecialForms do
alias Foo.Bar.Baz, as: Baz
We can also alias multiple modules in one line:
alias Foo.{Bar, Baz, Biz}
Is the same as:
alias Foo.Bar
alias Foo.Baz
alias Foo.Biz
## Lexical scope
`import/2`, `require/2` and `alias/2` are called directives and all
@@ -509,12 +517,13 @@ defmodule Kernel.SpecialForms do
defmacro alias(module, opts), do: error!([module, opts])
@doc """
Requires a module in order to use its macros.
Requires a given module to be compiled and loaded.
## Examples
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.
Notice that usually modules should not be required before usage,
the only exception is if you want to use the macros from a module.
In such cases, you need to explicitly require them.
Let's suppose you created your own `if/2` implementation in the module
`MyMacros`. If you want to invoke it, you need to first explicitly
@@ -645,7 +654,7 @@ defmodule Kernel.SpecialForms do
@doc """
Returns the current module name as an atom or `nil` otherwise.
Although the module can be accessed in the `__ENV__/0`, this macro
Although the module can be accessed in the `__ENV__`, this macro
is a convenient shortcut.
"""
defmacro __MODULE__, do: error!([])
@@ -1060,8 +1069,8 @@ defmodule Kernel.SpecialForms do
Hygiene.return_length #=> 3
Notice how `Hygiene.return_length/0` returns `3` even though the `Kernel.length/1`
function is not imported. In fact, even if `return_length/0`
Notice how `return_length` returns 3 even though the `length/1`
function is not imported. In fact, even if `return_length`
imported a function with the same name and arity from another
module, it wouldn't affect the function result:
@@ -1070,10 +1079,10 @@ defmodule Kernel.SpecialForms do
get_length
end
Calling this new `return_length/0` will still return `3` as result.
Calling this new `return_length` will still return 3 as result.
Elixir is smart enough to delay the resolution to the latest
possible moment. So, if you call `length([1, 2, 3])` inside quote,
moment possible. So, if you call `length([1, 2, 3])` inside quote,
but no `length/1` function is available, it is then expanded in
the caller:
@@ -1264,7 +1273,7 @@ defmodule Kernel.SpecialForms do
[2, 4, 6, 8]
# A comprehension with two generators
iex> for x <- [1, 2], y <- [2, 3], do: x * y
iex> for x <- [1, 2], y <- [2, 3], do: x*y
[2, 3, 4, 6]
Filters can also be given:
@@ -1286,7 +1295,7 @@ defmodule Kernel.SpecialForms do
need to organize bitstring streams:
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}
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}]
Variable assignments inside the comprehension, be it in generators,
@@ -1324,7 +1333,7 @@ defmodule Kernel.SpecialForms do
iex> opts = %{width: 10, height: 15}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, width * height}
...> do: {:ok, width * height}
{:ok, 150}
If all clauses match, the `do` block is executed, returning its result.
@@ -1333,14 +1342,14 @@ defmodule Kernel.SpecialForms do
iex> opts = %{width: 10}
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, width * height}
...> do: {:ok, width * height}
:error
Guards can be used in patterns as well:
iex> users = %{"melany" => "guest", "bob" => :admin}
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob"),
...> do: {:ok, to_string(role)}
...> do: {:ok, to_string(role)}
{:ok, "admin"}
As in `for/1`, variables bound inside `with/1` won't leak;
@@ -1351,7 +1360,7 @@ defmodule Kernel.SpecialForms do
iex> with {:ok, width} <- Map.fetch(opts, :width),
...> double_width = width * 2,
...> {:ok, height} <- Map.fetch(opts, :height),
...> do: {:ok, double_width * height}
...> do: {:ok, double_width * height}
{:ok, 300}
iex> width
nil
@@ -1616,7 +1625,7 @@ defmodule Kernel.SpecialForms do
defmacro cond(clauses), do: error!([clauses])
@doc ~S"""
Evaluates the given expressions and handles any error, exit,
Evaluates the given expressions and handles any error, exit
or throw that may have happened.
## Examples
@@ -1628,29 +1637,28 @@ defmodule Kernel.SpecialForms do
IO.puts "Invalid argument given"
catch
value ->
IO.puts "Caught #{inspect(value)}"
IO.puts "caught #{value}"
else
value ->
IO.puts "Success! The result was #{inspect(value)}"
IO.puts "Success! The result was #{value}"
after
IO.puts "This is printed regardless if it failed or succeed"
end
The `rescue` clause is used to handle exceptions, while the `catch`
clause can be used to catch thrown values and exits.
The `else` clause can be used to control flow based on the result of
the expression. `catch`, `rescue`, and `else` clauses work based on
pattern matching (similar to the `case` special form).
The rescue clause is used to handle exceptions, while the catch
clause can be used to catch thrown values. The else clause can
be used to control flow based on the result of the expression.
Catch, rescue and else clauses work based on pattern matching.
Note that calls inside `try/1` are not tail recursive since the VM
needs to keep the stacktrace in case an exception happens.
## `rescue` clauses
## Rescue clauses
Besides relying on pattern matching, `rescue` clauses provide some
conveniences around exceptions that allow one to rescue an
exception by its name. All the following formats are valid patterns
in `rescue` clauses:
Besides relying on pattern matching, rescue clauses provides some
conveniences around exceptions that allows one to rescue an
exception by its name. All the following formats are valid rescue
expressions:
try do
UndefinedModule.undefined_function
@@ -1680,7 +1688,7 @@ defmodule Kernel.SpecialForms do
## Erlang errors
Erlang errors are transformed into Elixir ones when rescuing:
Erlang errors are transformed into Elixir ones during rescue:
try do
:erlang.error(:badarg)
@@ -1699,7 +1707,7 @@ defmodule Kernel.SpecialForms do
end
In fact, `ErlangError` can be used to rescue any error that is
not a proper Elixir error. For example, it can be used to rescue
not an Elixir error proper. For example, it can be used to rescue
the earlier `:badarg` error too, prior to transformation:
try do
@@ -1710,45 +1718,25 @@ defmodule Kernel.SpecialForms do
## Catching throws and exits
The `catch` clause can be used to catch thrown values and exits.
The catch clause can be used to catch throws values and exits.
try do
exit(:shutdown)
catch
:exit, :shutdown ->
IO.puts "Exited with shutdown reason"
:exit, :shutdown -> IO.puts "Exited with shutdown reason"
end
try do
throw(:sample)
catch
:throw, :sample ->
IO.puts ":sample was thrown"
IO.puts "sample thrown"
end
The `catch` clause also supports `:error` alongside `:exit` and `:throw`, as
in Erlang, although it is commonly avoided in favor of `raise`/`rescue` control
mechanisms. One reason for this is that when catching `:error`, the error is
not automatically transformed into an Elixir error:
catch values also support `:error`, as in Erlang, although it is
commonly avoided in favor of raise/rescue control mechanisms.
try do
:erlang.error(:badarg)
catch
:error, :badarg ->
:ok
end
Note that it is possible to match both on the caught value as well as the *kind*
of such value:
try do
exit(:shutdown)
catch
kind, value when kind in [:exit, :throw] ->
IO.puts "Exited with or thrown value #{inspect(value)}"
end
## `after` clauses
## After clauses
An `after` clause allows you to define cleanup logic that will be invoked both
when the tried block of code succeeds and also when an error is raised. Note
@@ -1765,9 +1753,9 @@ defmodule Kernel.SpecialForms do
File.rm("tmp/story.txt")
end
## `else` clauses
## Else clauses
`else` clauses allow the result of the tried expression to be pattern
Else clauses allow the result of the expression to be pattern
matched on:
x = 2
@@ -1783,7 +1771,7 @@ defmodule Kernel.SpecialForms do
:large
end
If an `else` clause is not present and no exceptions are raised,
If an else clause is not present and no exceptions are raised,
the result of the expression will be returned:
x = 1
@@ -1795,9 +1783,9 @@ defmodule Kernel.SpecialForms do
:infinity
end
However, when an `else` clause is present but the result of the expression
does not match any of the patterns then an exception will be raised. This
exception will not be caught by a `catch` or `rescue` in the same `try`:
However when an else clause is present but the result of the expression
does not match any of the patterns an exception will be raised. This
exception will not be caught by a catch or rescue in the same try:
x = 1
try do
@@ -1817,8 +1805,8 @@ defmodule Kernel.SpecialForms do
:error_b
end
Similarly, an exception inside an `else` clause is not caught or rescued
inside the same `try`:
Similarly an exception inside an else clause is not caught or rescued
inside the same try:
try do
try do
@@ -1838,25 +1826,10 @@ defmodule Kernel.SpecialForms do
end
This means the VM no longer needs to keep the stacktrace once inside
an `else` clause and so tail recursion is possible when using a `try`
with a tail call as the final call inside an `else` clause. The same
an else clause and so tail recursion is possible when using a `try`
with a tail call as the final call inside an else clause. The same
is true for `rescue` and `catch` clauses.
Only the result of the tried expression falls down to the `else` clause.
If the `try` ends up in the `rescue` or `catch` clauses, their result
will not fall down to `else`:
try do
throw(:catch_this)
catch
:throw, :catch_this ->
:it_was_caught
else
# :it_was_caught will not fall down to this "else" clause.
other ->
{:else, other}
end
## Variable handling
Since an expression inside `try` may not have been evaluated
@@ -1908,7 +1881,7 @@ defmodule Kernel.SpecialForms do
end
An optional `after` clause can be given in case the message was not
received after the given timeout period, specified in milliseconds:
received after the specified timeout period:
receive do
{:selector, i, value} when is_integer(i) ->
@@ -1923,8 +1896,8 @@ defmodule Kernel.SpecialForms do
end
The `after` clause can be specified even if there are no match clauses.
The timeout value given to `after` can be any expression evaluating to
one of the allowed values:
The timeout value given to `after` can be a variable; two special
values are allowed:
* `:infinity` - the process should wait indefinitely for a matching
message, this is the same as not using a timeout
@@ -1932,10 +1905,6 @@ defmodule Kernel.SpecialForms do
* `0` - if there is no matching message in the mailbox, the timeout
will occur immediately
* positive integer smaller than `4_294_967_295` (`0xFFFFFFFF`
in hex notation) - it should be possible to represent the timeout
value as an unsigned 32-bit integer.
## Variables handling
The `receive/1` special form handles variables exactly as the `case/2`
+92 -118
View File
@@ -11,11 +11,8 @@ defmodule Kernel.Typespec do
"""
defmacro deftype(type) do
pos = :elixir_locals.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
Kernel.Typespec.deftype(:type, unquote(Macro.escape(type, unquote: true)),
unquote(line), unquote(file), unquote(module), unquote(pos))
Kernel.Typespec.deftype(:type, unquote(Macro.escape(type, unquote: true)), __ENV__)
end
end
@@ -29,11 +26,8 @@ defmodule Kernel.Typespec do
"""
defmacro defopaque(type) do
pos = :elixir_locals.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
Kernel.Typespec.deftype(:opaque, unquote(Macro.escape(type, unquote: true)),
unquote(line), unquote(file), unquote(module), unquote(pos))
Kernel.Typespec.deftype(:opaque, unquote(Macro.escape(type, unquote: true)), __ENV__)
end
end
@@ -47,11 +41,8 @@ defmodule Kernel.Typespec do
"""
defmacro deftypep(type) do
pos = :elixir_locals.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
Kernel.Typespec.deftype(:typep, unquote(Macro.escape(type, unquote: true)),
unquote(line), unquote(file), unquote(module), unquote(pos))
Kernel.Typespec.deftype(:typep, unquote(Macro.escape(type, unquote: true)), __ENV__)
end
end
@@ -65,11 +56,8 @@ defmodule Kernel.Typespec do
"""
defmacro defspec(spec) do
pos = :elixir_locals.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
Kernel.Typespec.defspec(:spec, unquote(Macro.escape(spec, unquote: true)),
unquote(line), unquote(file), unquote(module), unquote(pos))
Kernel.Typespec.defspec(:spec, unquote(Macro.escape(spec, unquote: true)), __ENV__)
end
end
@@ -83,14 +71,12 @@ defmodule Kernel.Typespec do
"""
defmacro defcallback(spec) do
pos = :elixir_locals.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
Kernel.Typespec.defspec(:callback, unquote(Macro.escape(spec, unquote: true)),
unquote(line), unquote(file), unquote(module), unquote(pos))
Kernel.Typespec.defspec(:callback, unquote(Macro.escape(spec, unquote: true)), __ENV__)
end
end
@doc """
Defines a macro callback.
This macro is responsible for handling the attribute `@macrocallback`.
@@ -101,22 +87,37 @@ defmodule Kernel.Typespec do
"""
defmacro defmacrocallback(spec) do
pos = :elixir_locals.cache_env(__CALLER__)
%{line: line, file: file, module: module} = __CALLER__
quote do
Kernel.Typespec.defspec(:macrocallback, unquote(Macro.escape(spec, unquote: true)),
unquote(line), unquote(file), unquote(module), unquote(pos))
Kernel.Typespec.defspec(:macrocallback, unquote(Macro.escape(spec, unquote: true)), __ENV__)
end
end
defmacro defoptional_callbacks(callbacks) do
quote do
Module.store_typespec(__ENV__.module, :optional_callbacks, {__ENV__.line, unquote(callbacks)})
end
end
@doc """
Defines a `type`, `typep` or `opaque` by receiving a typespec expression.
"""
def define_type(kind, expr, doc \\ nil, env) do
Module.store_typespec(env.module, kind, {kind, expr, doc, env})
end
@doc """
Defines a `spec` by receiving a typespec expression.
"""
def define_spec(kind, expr, env) do
defspec(kind, expr, env)
end
@doc """
Returns `true` if the current module defines a given type
(private, opaque or not). This function is only available
for modules being compiled.
"""
@spec defines_type?(module, atom, arity) :: boolean
def defines_type?(module, name, arity)
when is_atom(module) and is_atom(name) and arity in 0..255 do
def defines_type?(module, name, arity) do
finder = fn {_kind, expr, _caller} ->
type_to_signature(expr) == {name, arity}
end
@@ -128,9 +129,7 @@ defmodule Kernel.Typespec do
Returns `true` if the current module defines a given spec.
This function is only available for modules being compiled.
"""
@spec defines_spec?(module, atom, arity) :: boolean
def defines_spec?(module, name, arity)
when is_atom(module) and is_atom(name) and arity in 0..255 do
def defines_spec?(module, name, arity) do
finder = fn {_kind, expr, _caller} ->
spec_to_signature(expr) == {name, arity}
end
@@ -141,9 +140,7 @@ defmodule Kernel.Typespec do
Returns `true` if the current module defines a callback.
This function is only available for modules being compiled.
"""
@spec defines_callback?(module, atom, arity) :: boolean
def defines_callback?(module, name, arity)
when is_atom(module) and is_atom(name) and arity in 0..255 do
def defines_callback?(module, name, arity) do
finder = fn {_kind, expr, _caller} ->
spec_to_signature(expr) == {name, arity}
end
@@ -153,10 +150,8 @@ defmodule Kernel.Typespec do
@doc """
Converts a spec clause back to Elixir AST.
"""
@spec spec_to_ast(atom, tuple) :: {atom, keyword, [Macro.t]}
def spec_to_ast(name, spec)
def spec_to_ast(name, {:type, line, :fun, [{:type, _, :product, args}, result]})
when is_atom(name) do
def spec_to_ast(name, {:type, line, :fun, [{:type, _, :product, args}, result]}) do
meta = [line: line]
body = {name, meta, Enum.map(args, &typespec_to_ast/1)}
@@ -174,12 +169,11 @@ defmodule Kernel.Typespec do
end
end
def spec_to_ast(name, {:type, line, :fun, []}) when is_atom(name) do
def spec_to_ast(name, {:type, line, :fun, []}) do
{:::, [line: line], [{name, [line: line], []}, quote(do: term)]}
end
def spec_to_ast(name, {:type, line, :bounded_fun, [{:type, _, :fun, [{:type, _, :product, args}, result]}, constraints]})
when is_atom(name) do
def spec_to_ast(name, {:type, line, :bounded_fun, [{:type, _, :fun, [{:type, _, :product, args}, result]}, constraints]}) do
guards =
for {:type, _, :constraint, [{:atom, _, :is_subtype}, [{:var, _, var}, type]]} <- constraints do
{var, typespec_to_ast(type)}
@@ -212,7 +206,7 @@ defmodule Kernel.Typespec do
quote do: unquote(record)(unquote_splicing(args)) :: unquote(type)
end
def type_to_ast({name, type, args}) when is_atom(name) do
def type_to_ast({name, type, args}) do
args = for arg <- args, do: typespec_to_ast(arg)
quote do: unquote(name)(unquote_splicing(args)) :: unquote(typespec_to_ast(type))
end
@@ -228,12 +222,12 @@ defmodule Kernel.Typespec do
end
@doc """
Returns all types available from the module's BEAM code.
Returns all types available from the module's beam code.
The result is returned as a list of tuples where the first
element is the type (`:typep`, `:type` and `:opaque`).
The module must have a corresponding BEAM file which can be
The module must have a corresponding beam file which can be
located by the runtime system.
"""
@spec beam_types(module | binary) :: [tuple] | nil
@@ -256,12 +250,12 @@ defmodule Kernel.Typespec do
end
@doc """
Returns all specs available from the module's BEAM code.
Returns all specs available from the module's beam code.
The result is returned as a list of tuples where the first
element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file which can be
The module must have a corresponding beam file which can be
located by the runtime system.
"""
@spec beam_specs(module | binary) :: [tuple] | nil
@@ -270,12 +264,12 @@ defmodule Kernel.Typespec do
end
@doc """
Returns all callbacks available from the module's BEAM code.
Returns all callbacks available from the module's beam code.
The result is returned as a list of tuples where the first
element is spec name and arity and the second is the spec.
The module must have a corresponding BEAM file
The module must have a corresponding beam file
which can be located by the runtime system.
"""
@spec beam_callbacks(module | binary) :: [tuple] | nil
@@ -331,49 +325,50 @@ defmodule Kernel.Typespec do
## Macro callbacks
@doc false
def defspec(kind, expr, line, file, module, pos) when kind in [:callback, :macrocallback] do
def defspec(kind, expr, caller) when kind in [:callback, :macrocallback] do
case spec_to_signature(expr) do
{name, arity} ->
store_callbackdoc(line, file, module, kind, name, arity)
store_callbackdoc(caller, caller.module, kind, name, arity)
:error ->
:error
end
Module.store_typespec(module, kind, {kind, expr, pos})
Module.store_typespec(caller.module, kind, {kind, expr, caller})
end
@doc false
def defspec(kind, expr, _line, _file, module, pos) do
Module.store_typespec(module, kind, {kind, expr, pos})
def defspec(kind, expr, caller) do
Module.store_typespec(caller.module, kind, {kind, expr, caller})
end
defp store_callbackdoc(line, _file, module, kind, name, arity) do
defp store_callbackdoc(caller, module, kind, name, arity) do
table = :elixir_module.data_table(module)
{line, doc} = get_doc_info(table, :doc, line)
{line, doc} = get_doc_info(table, :doc, caller)
:ets.insert(table, {{:callbackdoc, {name, arity}}, line, kind, doc})
end
defp get_doc_info(table, attr, line) do
defp get_doc_info(table, attr, caller) do
case :ets.take(table, attr) do
[{^attr, {line, doc}, _, _}] -> {line, doc}
[] -> {line, nil}
[{^attr, {line, doc}}] -> {line, doc}
[] -> {caller.line, nil}
end
end
@doc false
def deftype(kind, expr, line, file, module, pos) do
def deftype(kind, expr, caller) do
module = caller.module
case type_to_signature(expr) do
{name, arity} -> store_typedoc(line, file, module, kind, name, arity)
{name, arity} -> store_typedoc(caller, caller.module, kind, name, arity)
:error -> :error
end
Module.store_typespec(module, kind, {kind, expr, pos})
Module.store_typespec(module, kind, {kind, expr, caller})
end
defp store_typedoc(line, file, module, kind, name, arity) do
defp store_typedoc(caller, module, kind, name, arity) do
table = :elixir_module.data_table(module)
{line, doc} = get_doc_info(table, :typedoc, line)
{line, doc} = get_doc_info(table, :typedoc, caller)
if kind == :typep && doc do
:elixir_errors.warn(line, file, "type #{name}/#{arity} is private, " <>
:elixir_errors.warn(caller.line, caller.file, "type #{name}/#{arity} is private, " <>
"@typedoc's are always discarded for private types")
end
@@ -383,9 +378,7 @@ defmodule Kernel.Typespec do
## Translation from Elixir AST to typespec AST
@doc false
def translate_type(kind, {:::, _, [{name, _, args}, definition]}, pos) when is_atom(name) and name != ::: do
caller = :elixir_locals.get_cached_env(pos)
def translate_type(kind, {:::, _, [{name, _, args}, definition]}, caller) when is_atom(name) and name != ::: do
args =
if is_atom(args) do
[]
@@ -406,37 +399,34 @@ defmodule Kernel.Typespec do
:opaque -> {:opaque, true}
end
if builtin_type?(name, arity) do
compile_error caller, "type #{name}/#{arity} is a builtin type and it cannot be redefined"
if elixir_builtin_type?(name, arity) do
:elixir_errors.handle_file_error(caller.file,
{caller.line, :erl_lint, {:builtin_type, {name, arity}}})
end
{{kind, {name, arity}, type}, caller.line, export}
end
def translate_type(_kind, other, pos) do
caller = :elixir_locals.get_cached_env(pos)
def translate_type(_kind, other, caller) do
type_spec = Macro.to_string(other)
compile_error caller, "invalid type specification: #{type_spec}"
end
defp builtin_type?(:as_boolean, 1), do: true
defp builtin_type?(:struct, 0), do: true
defp builtin_type?(:charlist, 0), do: true
# TODO: Remove char_list type by 2.0
defp builtin_type?(:char_list, 0), do: true
defp builtin_type?(:nonempty_charlist, 0), do: true
defp builtin_type?(:keyword, 0), do: true
defp builtin_type?(:keyword, 1), do: true
defp builtin_type?(name, arity), do: :erl_internal.is_type(name, arity)
defp elixir_builtin_type?(:as_boolean, 1), do: true
defp elixir_builtin_type?(:struct, 0), do: true
defp elixir_builtin_type?(:charlist, 0), do: true
# TODO: Deprecate char_list type by v1.5
defp elixir_builtin_type?(:char_list, 0), do: true
defp elixir_builtin_type?(:keyword, 0), do: true
defp elixir_builtin_type?(:keyword, 1), do: true
defp elixir_builtin_type?(_, _), do: false
@doc false
def translate_spec(kind, {:when, _meta, [spec, guard]}, pos) do
caller = :elixir_locals.get_cached_env(pos)
def translate_spec(kind, {:when, _meta, [spec, guard]}, caller) do
translate_spec(kind, spec, guard, caller)
end
def translate_spec(kind, spec, pos) do
caller = :elixir_locals.get_cached_env(pos)
def translate_spec(kind, spec, caller) do
translate_spec(kind, spec, [], caller)
end
@@ -457,6 +447,8 @@ defmodule Kernel.Typespec do
defp translate_spec(kind, meta, name, args, return, guard, caller) when is_atom(args),
do: translate_spec(kind, meta, name, [], return, guard, caller)
defp translate_spec(:macrocallback, meta, name, args, return, guard, caller),
do: translate_spec(:callback, meta, :"MACRO-#{name}", macro_args(args), return, guard, caller)
defp translate_spec(kind, meta, name, args, return, guard, caller) do
ensure_no_defaults!(args)
@@ -478,6 +470,10 @@ defmodule Kernel.Typespec do
{{kind, {name, arity}, spec}, caller.line}
end
defp macro_args(args) do
[quote(do: {line :: Macro.Env.line, env :: Macro.Env.t}) | args]
end
defp ensure_no_defaults!(args) do
:lists.foreach fn
{:::, _, [left, right]} ->
@@ -582,7 +578,7 @@ defmodule Kernel.Typespec do
defp typespec_to_ast({:type, line, :map, fields}) do
fields = Enum.map fields, fn
{:type, _, :map_field_assoc, :any} ->
{{:optional, [], [{:any, [], []}]}, {:any, [], []}}
{:..., [line: line], nil}
{:type, _, :map_field_exact, [{:atom, _, k}, v]} ->
{k, typespec_to_ast(v)}
{:type, _, :map_field_exact, [k, v]} ->
@@ -628,7 +624,7 @@ defmodule Kernel.Typespec do
end
defp typespec_to_ast({:type, line, :fun, []}) do
typespec_to_ast({:type, line, :fun, [{:type, line, :any}, {:type, line, :any, []}]})
typespec_to_ast({:type, line, :fun, [{:type, line, :any}, {:type, line, :any, []} ]})
end
defp typespec_to_ast({:type, line, :range, [left, right]}) do
@@ -653,16 +649,12 @@ defmodule Kernel.Typespec do
end
# Special shortcut(s)
# TODO: Remove char_list type by 2.0
# TODO: Deprecate char_list type by v1.5
defp typespec_to_ast({:remote_type, line, [{:atom, _, :elixir}, {:atom, _, type}, []]})
when type in [:charlist, :char_list] do
typespec_to_ast({:type, line, :charlist, []})
end
defp typespec_to_ast({:remote_type, line, [{:atom, _, :elixir}, {:atom, _, :nonempty_charlist}, []]}) do
typespec_to_ast({:type, line, :nonempty_charlist, []})
end
defp typespec_to_ast({:remote_type, line, [{:atom, _, :elixir}, {:atom, _, :struct}, []]}) do
typespec_to_ast({:type, line, :struct, []})
end
@@ -758,6 +750,8 @@ defmodule Kernel.Typespec do
defp typespec({:%{}, meta, fields} = map, vars, caller) do
fields =
:lists.map(fn
:... ->
{:type, line(meta), :map_field_assoc, :any}
{k, v} when is_atom(k) ->
{:type, line(meta), :map_field_exact, [typespec(k, vars, caller), typespec(v, vars, caller)]}
{{:required, meta2, [k]}, v} ->
@@ -765,7 +759,7 @@ defmodule Kernel.Typespec do
{{:optional, meta2, [k]}, v} ->
{:type, line(meta2), :map_field_assoc, [typespec(k, vars, caller), typespec(v, vars, caller)]}
{k, v} ->
# TODO: Emit warnings on v1.6 (when we drop OTP 18 support)
# TODO: Emit warnings on v1.5
# :elixir_errors.warn(caller.line, caller.file,
# "invalid map specification. %{foo => bar} is deprecated in favor of " <>
# "%{required(foo) => bar} and %{optional(foo) => bar}. required/1 is an " <>
@@ -895,7 +889,7 @@ defmodule Kernel.Typespec do
defp typespec({{:., meta, [remote, name]}, _, args} = orig, vars, caller) do
# We set a function name to avoid tracking
# aliases in typespecs as compile time dependencies.
remote = Macro.expand(remote, %{caller | function: {:typespec, 0}})
remote = Macro.expand remote, %{caller | function: {:typespec, 0}}
unless is_atom(remote) do
compile_error(caller, "invalid remote in typespec: #{Macro.to_string(orig)}")
end
@@ -903,7 +897,7 @@ defmodule Kernel.Typespec do
end
# Handle tuples
defp typespec({:tuple, meta, []}, _vars, _caller) do
defp typespec({:tuple, meta, args}, _vars, _caller) when args == [] or is_atom(args) do
{:type, line(meta), :tuple, :any}
end
@@ -931,38 +925,18 @@ defmodule Kernel.Typespec do
end
# Handle local calls
defp typespec({:string, meta, arguments}, vars, caller) do
:elixir_errors.warn caller.line, caller.file,
"string() type use is discouraged. " <>
"For character lists, use charlist() type, for strings, String.t()\n" <>
Exception.format_stacktrace(Macro.Env.stacktrace(caller))
defp typespec({type, meta, arguments}, vars, caller) when type in [:string, :nonempty_string] do
:elixir_errors.warn caller.line, caller.file, "#{type}() type use is discouraged. For character lists, use " <>
"charlist() type, for strings, String.t()\n#{Exception.format_stacktrace(Macro.Env.stacktrace(caller))}"
arguments = for arg <- arguments, do: typespec(arg, vars, caller)
{:type, line(meta), :string, arguments}
{:type, line(meta), type, arguments}
end
defp typespec({:nonempty_string, meta, arguments}, vars, caller) do
:elixir_errors.warn caller.line, caller.file,
"nonempty_string() type use is discouraged. " <>
"For non-empty character lists, use nonempty_charlist() type, for strings, String.t()\n" <>
Exception.format_stacktrace(Macro.Env.stacktrace(caller))
arguments = for arg <- arguments, do: typespec(arg, vars, caller)
{:type, line(meta), :nonempty_string, arguments}
end
# TODO: Remove char_list type by 2.0
# TODO: Deprecate char_list type by v1.5
defp typespec({type, _meta, []}, vars, caller) when type in [:charlist, :char_list] do
if type == :char_list do
:elixir_errors.warn caller.line, caller.file, "the char_list() type is deprecated, use charlist()"
end
typespec((quote do: :elixir.charlist()), vars, caller)
end
defp typespec({:nonempty_charlist, _meta, []}, vars, caller) do
typespec((quote do: :elixir.nonempty_charlist()), vars, caller)
end
defp typespec({:struct, _meta, []}, vars, caller) do
typespec((quote do: :elixir.struct()), vars, caller)
end
@@ -1032,7 +1006,7 @@ defmodule Kernel.Typespec do
defp remote_type({remote, meta, name, arguments}, vars, caller) do
arguments = for arg <- arguments, do: typespec(arg, vars, caller)
{:remote_type, line(meta), [remote, name, arguments]}
{:remote_type, line(meta), [ remote, name, arguments ]}
end
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
+31 -33
View File
@@ -6,10 +6,8 @@ defmodule Kernel.Utils do
@doc """
Callback for destructure.
"""
def destructure(list, count) when is_list(list) and is_integer(count) and count >= 0,
do: destructure_list(list, count)
def destructure(nil, count) when is_integer(count) and count >= 0,
do: destructure_nil(count)
def destructure(list, count) when is_list(list), do: destructure_list(list, count)
def destructure(nil, count), do: destructure_nil(count)
defp destructure_list(_, 0), do: []
defp destructure_list([], count), do: destructure_nil(count)
@@ -21,9 +19,8 @@ defmodule Kernel.Utils do
@doc """
Callback for defdelegate.
"""
def defdelegate(fun, opts) when is_list(opts) do
# TODO: Remove by 2.0
append_first? = Keyword.get(opts, :append_first, false)
def defdelegate(fun, opts) do
append_first = Keyword.get(opts, :append_first, false)
{name, args} =
case Macro.decompose_call(fun) do
@@ -31,30 +28,38 @@ defmodule Kernel.Utils do
_ -> raise ArgumentError, "invalid syntax in defdelegate #{Macro.to_string(fun)}"
end
as_args_list = normalize_args(args)
as = Keyword.get(opts, :as, name)
as_args = build_as_args(args, append_first?)
{name, args, as, as_args}
:lists.map(fn as_args ->
formal_args = make_formal_args(as_args)
as_args = case append_first do
true -> tl(as_args) ++ [hd(as_args)]
false -> as_args
end
{name, formal_args, as, as_args}
end, as_args_list)
end
defp build_as_args(args, append_first?) do
as_args = :lists.map(&build_as_arg/1, args)
case append_first? do
true -> tl(as_args) ++ [hd(as_args)]
false -> as_args
end
defp make_formal_args(args) do
fun = &match?({name, _, mod} when is_atom(name) and is_atom(mod), &1)
:lists.filter(fun, args)
end
defp build_as_arg({:\\, _, [arg, _default_arg]}), do: validate_arg(arg)
defp build_as_arg(arg), do: validate_arg(arg)
defp validate_arg({name, _, mod} = arg) when is_atom(name) and is_atom(mod) do
arg
defp normalize_args(raw_args) do
:lists.foldr(fn
({:\\, _, [arg, default_arg]}, [as_args | _] = as_args_list) ->
new_as_args = [default_arg | as_args]
[new_as_args | add_arg(as_args_list, arg)]
(arg, as_args_list) ->
add_arg(as_args_list, arg)
end, [[]], raw_args)
end
defp validate_arg(ast) do
raise ArgumentError, "defdelegate/2 only accepts function parameters, got: #{Macro.to_string(ast)}"
defp add_arg(as_args_list, {name, _, mod} = arg) when is_atom(name) and is_atom(mod),
do: :lists.map(&([arg | &1]), as_args_list)
defp add_arg(_, code) do
raise ArgumentError,
"defdelegate/2 only accepts function parameters, got: #{Macro.to_string(code)}"
end
@doc """
@@ -84,15 +89,8 @@ defmodule Kernel.Utils do
raise ArgumentError, "struct field names must be atoms, got: #{inspect other}"
end, fields)
enforce_keys = List.wrap(Module.get_attribute(module, :enforce_keys))
:lists.foreach(fn
key when is_atom(key) -> :ok
key -> raise ArgumentError, "keys given to @enforce_keys must be atoms, got: #{inspect key}"
end, enforce_keys)
{:maps.put(:__struct__, module, :maps.from_list(fields)),
enforce_keys,
List.wrap(Module.get_attribute(module, :enforce_keys)),
Module.get_attribute(module, :derive)}
end
@@ -119,7 +117,7 @@ defmodule Kernel.Utils do
exception
end
def raise(other) do
ArgumentError.exception("raise/1 expects a module name, string or exception as " <>
ArgumentError.exception("raise/1 expects an alias, string or exception as " <>
"the first argument, got: #{inspect other}")
end
end
+25 -136
View File
@@ -6,31 +6,6 @@ defmodule Keyword do
element of the tuple is an atom and the second element
can be any value.
For example, the following is a keyword list:
[{:exit_on_close, true}, {:active, :once}, {:packet_size, 1024}]
Elixir provides a special and more concise syntax for keyword lists
that looks like this:
[exit_on_close: true, active: :once, packet_size: 1024]
This is also the syntax that Elixir uses to inspect keyword lists:
iex> [{:active, :once}]
[active: :once]
The two syntaxes are completely equivalent. Note that when keyword
lists are passed as the last argument to a function, if the short-hand
syntax is used then the square brackets around the keyword list can
be omitted as well. For example, the following:
String.split("1-0", "-", trim: true, parts: 2)
is equivalent to:
String.split("1-0", "-", [trim: true, parts: 2])
A keyword may have duplicated keys so it is not strictly
a key-value store. However most of the functions in this module
behave exactly as a dictionary so they work similarly to
@@ -40,10 +15,6 @@ defmodule Keyword do
the given key, regardless if duplicated entries exist.
Similarly, `Keyword.put/3` and `Keyword.delete/3` ensure all
duplicated entries for a given key are removed when invoked.
Note that operations that require keys to be found in the keyword
list (like `Keyword.get/3`) need to traverse the list in order
to find keys, so these operations may be slower than their map
counterparts.
A handful of functions exist to handle duplicated keys, in
particular, `Enum.into/2` allows creating new keywords without
@@ -51,10 +22,10 @@ defmodule Keyword do
a given key and `delete_first/2` deletes just one of the existing
entries.
The functions in `Keyword` do not guarantee any property when
The functions in Keyword do not guarantee any property when
it comes to ordering. However, since a keyword list is simply a
list, all the operations defined in `Enum` and `List` can be
applied too, especially when ordering is required.
applied too, specially when ordering is required.
"""
@compile :inline_list_funcs
@@ -174,6 +145,7 @@ defmodule Keyword do
3
"""
@spec get(t, key) :: value
@spec get(t, key, value) :: value
def get(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do
@@ -254,26 +226,18 @@ defmodule Keyword do
defp get_and_update([{key, current} | t], acc, key, fun) do
case fun.(current) do
{get, value} ->
{get, :lists.reverse(acc, [{key, value} | t])}
:pop ->
{current, :lists.reverse(acc, t)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
{get, value} -> {get, :lists.reverse(acc, [{key, value} | t])}
:pop -> {current, :lists.reverse(acc, t)}
end
end
defp get_and_update([{_, _} = h | t], acc, key, fun),
defp get_and_update([h | t], acc, key, fun),
do: get_and_update(t, [h | acc], key, fun)
defp get_and_update([], acc, key, fun) do
case fun.(nil) do
{get, update} ->
{get, [{key, update} | :lists.reverse(acc)]}
:pop ->
{nil, :lists.reverse(acc)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
{get, update} -> {get, [{key, update} | :lists.reverse(acc)]}
:pop -> {nil, :lists.reverse(acc)}
end
end
@@ -317,8 +281,6 @@ defmodule Keyword do
{get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
:pop ->
{value, :lists.reverse(acc, keywords)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
end
end
@@ -562,49 +524,6 @@ defmodule Keyword do
end
end
@doc """
Alters the value stored under `key` to `value`, but only
if the entry `key` already exists in the keyword list.
In the case a value is stored multiple times in the keyword list,
later occurrences are removed.
## Examples
iex> Keyword.replace([a: 1], :b, 2)
[a: 1]
iex> Keyword.replace([a: 1, b: 2, a: 4], :a, 3)
[a: 3, b: 2]
"""
@spec replace(t, key, value) :: t
def replace(keywords, key, value) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do
{^key, _} -> [{key, value} | delete(keywords, key)]
false -> keywords
end
end
@doc """
Similar to `replace/3`, but will raise a `KeyError`
if the entry `key` does not exist.
## Examples
iex> Keyword.replace!([a: 1, b: 2, a: 4], :a, 3)
[a: 3, b: 2]
iex> Keyword.replace!([a: 1], :b, 2)
** (KeyError) key :b not found in: [a: 1]
"""
@spec replace!(t, key, value) :: t
def replace!(keywords, key, value) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do
{^key, _} -> [{key, value} | delete(keywords, key)]
false -> raise KeyError, key: key, term: keywords
end
end
@doc """
Checks if two keywords are equal.
@@ -642,23 +561,11 @@ defmodule Keyword do
iex> Keyword.merge([a: 1, b: 2], [a: 3, d: 4, a: 5])
[b: 2, a: 3, d: 4, a: 5]
iex> Keyword.merge([a: 1], [2, 3])
** (ArgumentError) expected a keyword list as the second argument, got: [2, 3]
"""
@spec merge(t, t) :: t
def merge(keywords1, keywords2) when is_list(keywords1) and is_list(keywords2) do
if keyword?(keywords2) do
fun = fn
{key, _value} when is_atom(key) ->
not has_key?(keywords2, key)
_ ->
raise ArgumentError, message: "expected a keyword list as the first argument, got: #{inspect keywords1}"
end
:lists.filter(fun, keywords1) ++ keywords2
else
raise ArgumentError, message: "expected a keyword list as the second argument, got: #{inspect keywords2}"
end
fun = fn {k, _v} -> not has_key?(keywords2, k) end
:lists.filter(fun, keywords1) ++ keywords2
end
@doc """
@@ -689,40 +596,26 @@ defmodule Keyword do
...> end)
[b: 2, a: 4, d: 4, a: 8]
iex> Keyword.merge([a: 1, b: 2], [:a, :b], fn :a, v1, v2 ->
...> v1 + v2
...> end)
** (ArgumentError) expected a keyword list as the second argument, got: [:a, :b]
"""
@spec merge(t, t, (key, value, value -> value)) :: t
def merge(keywords1, keywords2, fun) when is_list(keywords1) and is_list(keywords2) and is_function(fun, 3) do
if keyword?(keywords1) do
do_merge(keywords2, [], keywords1, keywords1, fun, keywords2)
else
raise ArgumentError, message: "expected a keyword list as the first argument, got: #{inspect keywords1}"
end
def merge(keywords1, keywords2, fun) when is_list(keywords1) and is_list(keywords2) do
do_merge(keywords2, [], keywords1, keywords1, fun)
end
defp do_merge([{key, value2} | tail], acc, rest, original, fun, keywords2) when is_atom(key) do
case :lists.keyfind(key, 1, original) do
{^key, value1} ->
do_merge(tail, [{key, fun.(key, value1, value2)} | acc],
delete(rest, key), :lists.keydelete(key, 1, original), fun, keywords2)
defp do_merge([{k, v2} | t], acc, rest, original, fun) do
case :lists.keyfind(k, 1, original) do
{^k, v1} ->
do_merge(t, [{k, fun.(k, v1, v2)} | acc],
delete(rest, k), :lists.keydelete(k, 1, original), fun)
false ->
do_merge(tail, [{key, value2} | acc], rest, original, fun, keywords2)
do_merge(t, [{k, v2} | acc], rest, original, fun)
end
end
defp do_merge([], acc, rest, _original, _fun, _keywords2) do
defp do_merge([], acc, rest, _original, _fun) do
rest ++ :lists.reverse(acc)
end
defp do_merge(_other, _acc, _rest, _original, _fun, keywords2) do
raise ArgumentError, message: "expected a keyword list as the second argument, got: #{inspect keywords2}"
end
@doc """
Returns whether a given `key` exists in the given `keywords`.
@@ -826,7 +719,6 @@ defmodule Keyword do
{[a: 1, c: 3, a: 4], [b: 2]}
"""
@spec split(t, [key]) :: {t, t}
def split(keywords, keys) when is_list(keywords) do
fun = fn {k, v}, {take, drop} ->
case k in keys do
@@ -854,7 +746,6 @@ defmodule Keyword do
[a: 1, c: 3, a: 5]
"""
@spec take(t, [key]) :: t
def take(keywords, keys) when is_list(keywords) do
:lists.filter(fn {k, _} -> k in keys end, keywords)
end
@@ -872,9 +763,8 @@ defmodule Keyword do
[a: 1, c: 3, a: 5]
"""
@spec drop(t, [key]) :: t
def drop(keywords, keys) when is_list(keywords) do
:lists.filter(fn {key, _} -> key not in keys end, keywords)
:lists.filter(fn {k, _} -> not k in keys end, keywords)
end
@doc """
@@ -945,13 +835,13 @@ defmodule Keyword do
## Examples
iex> Keyword.pop_first([a: 1], :a)
iex> Keyword.pop_first [a: 1], :a
{1, []}
iex> Keyword.pop_first([a: 1], :b)
iex> Keyword.pop_first [a: 1], :b
{nil, [a: 1]}
iex> Keyword.pop_first([a: 1], :b, 3)
iex> Keyword.pop_first [a: 1], :b, 3
{3, [a: 1]}
iex> Keyword.pop_first([a: 1, a: 2], :a)
iex> Keyword.pop_first [a: 1, a: 2], :a
{1, [a: 2]}
"""
@@ -972,15 +862,14 @@ defmodule Keyword do
[a: 1]
"""
@spec to_list(t) :: t
def to_list(keyword) when is_list(keyword) do
keyword
end
@doc false
# TODO: Remove on 2.0
# (hard-deprecated in elixir_dispatch)
def size(keyword) do
IO.warn "Keyword.size/1 is deprecated, please use Kernel.length/1"
length(keyword)
end
end
+60 -281
View File
@@ -1,66 +1,24 @@
defmodule List do
@moduledoc """
Functions that work on (linked) lists.
Specialized functions that only work on lists.
Lists in Elixir are specified between square brackets:
In general, favor using the `Enum` API instead of `List`.
iex> [1, "two", 3, :four]
[1, "two", 3, :four]
Index access for list is linear. Negative indexes are also
supported but they imply the list will be iterated twice,
one to calculate the proper index and another to perform the
operation.
Two lists can be concatenated and subtracted using the
`Kernel.++/2` and `Kernel.--/2` operators:
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]
Lists in Elixir are effectively linked lists, which means
they are internally represented in pairs containing the
head and the tail of a list:
iex> [head | tail] = [1, 2, 3]
iex> head
1
iex> tail
[2, 3]
Similarly, we could write the list `[1, 2, 3]` using only
such pairs (called cons cells):
iex> [1 | [2 | [3 | []]]]
[1, 2, 3]
Some lists, called improper lists, do not have an empty list as
the second element in the last cons cell:
iex> [1 | [2 | [3 | 4]]]
[1, 2, 3 | 4]
Although improper lists are generally avoided, they are used in some
special circumstances like iodata and chardata entities (see the `IO` module).
Due to their cons cell based representation, prepending an element
to a list is always fast (constant time), while appending becomes
slower as the list grows in size (linear time):
iex> list = [1, 2, 3]
iex> [0 | list] # fast
[0, 1, 2, 3]
iex> list ++ [4] # slow
[1, 2, 3, 4]
The `Kernel` module contains many functions to manipulate lists
and that are allowed in guards. For example, `Kernel.hd/1` to
retrieve the head, `Kernel.tl/1` to fetch the tail and
`Kernel.length/1` for calculating the length. Keep in mind that,
similar to appending to a list, calculating the length needs to
traverse the whole list.
A decision was taken to delegate most functions to
Erlang's standard library but follow Elixir's convention
of receiving the subject (in this case, a list) as the
first argument.
## Charlists
If a list is made of non-negative integers, it can also be called
a charlist. Elixir uses single quotes to define charlists:
If a list is made of non-negative integers, it can also
be called as a charlist. Elixir uses single quotes to
define charlists:
iex> 'héllo'
[104, 233, 108, 108, 111]
@@ -74,7 +32,7 @@ defmodule List do
The rationale behind this behaviour is to better support
Erlang libraries which may return text as charlists
instead of Elixir strings. One example of such functions
is `Application.loaded_applications/0`:
is `Application.loaded_applications`:
Application.loaded_applications
#=> [{:stdlib, 'ERTS CXC 138 10', '2.6'},
@@ -82,44 +40,28 @@ defmodule List do
{:elixir, 'elixir', '1.0.0'},
{:kernel, 'ERTS CXC 138 10', '4.1'},
{:logger, 'logger', '1.0.0'}]
## List and Enum modules
This module aims to provide operations that are specific
to lists, like conversion between data types, updates,
deletions and key lookups (for lists of tuples). For traversing
lists in general, developers should use the functions in the
`Enum` module that work across a variety of data types.
In both `Enum` and `List` modules, any kind of index access
on a list is linear. Negative indexes are also supported but
they imply the list will be iterated twice, one to calculate
the proper index and another to perform the operation.
"""
@compile :inline_list_funcs
@doc """
Deletes the given `item` from the `list`. Returns a new list without
the item.
If the `item` occurs more than once in the `list`, just
Deletes the given item from the list. Returns a list without
the item. If the item occurs more than once in the list, just
the first occurrence is removed.
## Examples
iex> List.delete([:a, :b, :c], :a)
[:b, :c]
iex> List.delete([1, 2, 3], 1)
[2, 3]
iex> List.delete([:a, :b, :b, :c], :b)
[:a, :b, :c]
iex> List.delete([1, 2, 2, 3], 2)
[1, 2, 3]
"""
@spec delete(list, any) :: list
def delete(list, item)
def delete([item | list], item), do: list
def delete([other | list], item), do: [other | delete(list, item)]
def delete([], _item), do: []
def delete(list, item) do
:lists.delete(item, list)
end
@doc """
Duplicates the given element `n` times in a list.
@@ -132,6 +74,7 @@ defmodule List do
iex> List.duplicate([1, 2], 2)
[[1, 2], [1, 2]]
"""
@spec duplicate(elem, non_neg_integer) :: [elem] when elem: var
def duplicate(elem, n) do
@@ -218,7 +161,7 @@ defmodule List do
"""
@spec first([elem]) :: nil | elem when elem: var
def first([]), do: nil
def first([head | _]), do: head
def first([h | _]), do: h
@doc """
Returns the last element in `list` or `nil` if `list` is empty.
@@ -236,9 +179,9 @@ defmodule List do
"""
@spec last([elem]) :: nil | elem when elem: var
def last([]), do: nil
def last([head]), do: head
def last([_ | tail]), do: last(tail)
def last([]), do: nil
def last([h]), do: h
def last([_ | t]), do: last(t)
@doc """
Receives a list of tuples and returns the first tuple
@@ -279,7 +222,7 @@ defmodule List do
false
"""
@spec keymember?([tuple], any, non_neg_integer) :: boolean
@spec keymember?([tuple], any, non_neg_integer) :: any
def keymember?(list, key, position) do
:lists.keymember(key, position + 1, list)
end
@@ -318,10 +261,9 @@ defmodule List do
end
@doc """
Receives a `list` of tuples and replaces the item
identified by `key` at `position`.
If the item does not exist, it is added to the end of the `list`.
Receives a list of tuples and replaces the item
identified by `key` at `position`. If the item
does not exist, it is added to the end of the list.
## Examples
@@ -338,7 +280,7 @@ defmodule List do
end
@doc """
Receives a `list` of tuples and deletes the first tuple
Receives a list of tuples and deletes the first tuple
where the item at `position` matches the
given `key`. Returns the new list.
@@ -388,7 +330,6 @@ defmodule List do
@doc """
Wraps the argument in a list.
If the argument is already a list, returns the list.
If the argument is `nil`, returns an empty list.
@@ -439,9 +380,8 @@ defmodule List do
@doc """
Returns a list with `value` inserted at the specified `index`.
Note that `index` is capped at the list length. Negative indices
indicate an offset from the end of the `list`.
indicate an offset from the end of the list.
## Examples
@@ -459,7 +399,7 @@ defmodule List do
"""
@spec insert_at(list, integer, any) :: list
def insert_at(list, index, value) when is_integer(index) do
def insert_at(list, index, value) do
if index < 0 do
do_insert_at(list, length(list) + index + 1, value)
else
@@ -469,8 +409,7 @@ defmodule List do
@doc """
Returns a list with a replaced value at the specified `index`.
Negative indices indicate an offset from the end of the `list`.
Negative indices indicate an offset from the end of the list.
If `index` is out of bounds, the original `list` is returned.
## Examples
@@ -489,7 +428,7 @@ defmodule List do
"""
@spec replace_at(list, integer, any) :: list
def replace_at(list, index, value) when is_integer(index) do
def replace_at(list, index, value) do
if index < 0 do
do_replace_at(list, length(list) + index, value)
else
@@ -499,8 +438,7 @@ defmodule List do
@doc """
Returns a list with an updated value at the specified `index`.
Negative indices indicate an offset from the end of the `list`.
Negative indices indicate an offset from the end of the list.
If `index` is out of bounds, the original `list` is returned.
## Examples
@@ -519,7 +457,7 @@ defmodule List do
"""
@spec update_at([elem], integer, (elem -> any)) :: list when elem: var
def update_at(list, index, fun) when is_function(fun, 1) and is_integer(index) do
def update_at(list, index, fun) do
if index < 0 do
do_update_at(list, length(list) + index, fun)
else
@@ -529,8 +467,7 @@ defmodule List do
@doc """
Produces a new list by removing the value at the specified `index`.
Negative indices indicate an offset from the end of the `list`.
Negative indices indicate an offset from the end of the list.
If `index` is out of bounds, the original `list` is returned.
## Examples
@@ -546,69 +483,14 @@ defmodule List do
"""
@spec delete_at(list, integer) :: list
def delete_at(list, index) when is_integer(index) do
elem(pop_at(list, index), 1)
end
@doc """
Returns and removes the value at the specified `index` in the `list`.
Negative indices indicate an offset from the end of the `list`.
If `index` is out of bounds, the original `list` is returned.
## Examples
iex> List.pop_at([1, 2, 3], 0)
{1, [2, 3]}
iex> List.pop_at([1, 2, 3], 5)
{nil, [1, 2, 3]}
iex> List.pop_at([1, 2, 3], 5, 10)
{10, [1, 2, 3]}
iex> List.pop_at([1, 2, 3], -1)
{3, [1, 2]}
"""
@spec pop_at(list, integer, any) :: {any, list}
def pop_at(list, index, default \\ nil) when is_integer(index) do
def delete_at(list, index) do
if index < 0 do
do_pop_at(list, length(list) + index, default, [])
do_delete_at(list, length(list) + index)
else
do_pop_at(list, index, default, [])
do_delete_at(list, index)
end
end
@doc """
Returns `true` if `list` starts with the given `prefix` list; otherwise returns `false`.
If `prefix` is an empty list, it returns `true`.
### Examples
iex> List.starts_with?([1, 2, 3], [1, 2])
true
iex> List.starts_with?([1, 2], [1, 2, 3])
false
iex> List.starts_with?([:alpha], [])
true
iex> List.starts_with?([], [:alpha])
false
"""
@spec starts_with?(list, list) :: boolean
@spec starts_with?(list, []) :: true
@spec starts_with?([], nonempty_list) :: false
def starts_with?(list, prefix)
def starts_with?([head | tail], [head | prefix_tail]),
do: starts_with?(tail, prefix_tail);
def starts_with?(list, []) when is_list(list),
do: true
def starts_with?(list, [_ | _]) when is_list(list),
do: false
@doc """
Converts a charlist to an atom.
@@ -764,109 +646,6 @@ defmodule List do
end
end
@doc """
Returns a keyword list that represents an *edit script*.
The algorithm is outlined in the
"An O(ND) Difference Algorithm and Its Variations" paper by E. Myers.
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
corresponding key is `:del`), or left alone (if the corresponding key is
`:eq`) in `list1` in order to be closer to `list2`.
## Examples
iex> List.myers_difference([1, 4, 2, 3], [1, 2, 3, 4])
[eq: [1], del: [4], eq: [2, 3], ins: [4]]
"""
@spec myers_difference(list, list) :: [{:eq | :ins | :del, list}] | nil
def myers_difference(list1, list2) when is_list(list1) and is_list(list2) do
path = {0, 0, list1, list2, []}
find_script(0, length(list1) + length(list2), [path])
end
defp find_script(envelope, max, _paths) when envelope > max do
nil
end
defp find_script(envelope, max, paths) do
case each_diagonal(-envelope, envelope, paths, []) do
{:done, edits} -> compact_reverse(edits, [])
{:next, paths} -> find_script(envelope + 1, max, paths)
end
end
defp compact_reverse([], acc), do: acc
defp compact_reverse([{kind, elem} | rest], [{kind, result} | acc]) do
compact_reverse(rest, [{kind, [elem | result]} | acc])
end
defp compact_reverse([{kind, elem} | rest], acc) do
compact_reverse(rest, [{kind, [elem]} | acc])
end
defp each_diagonal(diag, limit, _paths, next_paths) when diag > limit do
{:next, Enum.reverse(next_paths)}
end
defp each_diagonal(diag, limit, paths, next_paths) do
{path, rest} = proceed_path(diag, limit, paths)
with {:cont, path} <- follow_snake(path) do
each_diagonal(diag + 2, limit, rest, [path | next_paths])
end
end
defp proceed_path(0, 0, [path]), do: {path, []}
defp proceed_path(diag, limit, [path | _] = paths) when diag == -limit do
{move_down(path), paths}
end
defp proceed_path(diag, limit, [path]) when diag == limit do
{move_right(path), []}
end
defp proceed_path(_diag, _limit, [path1, path2 | rest]) do
if elem(path1, 1) > elem(path2, 1) do
{move_right(path1), [path2 | rest]}
else
{move_down(path2), [path2 | rest]}
end
end
defp move_right({x, y, list1, [elem | rest], edits}) do
{x + 1, y, list1, rest, [{:ins, elem} | edits]}
end
defp move_right({x, y, list1, [], edits}) do
{x + 1, y, list1, [], edits}
end
defp move_down({x, y, [elem | rest], list2, edits}) do
{x, y + 1, rest, list2, [{:del, elem} | edits]}
end
defp move_down({x, y, [], list2, edits}) do
{x, y + 1, [], list2, edits}
end
defp follow_snake({x, y, [elem | rest1], [elem | rest2], edits}) do
follow_snake({x + 1, y + 1, rest1, rest2, [{:eq, elem} | edits]})
end
defp follow_snake({_x, _y, [], [], edits}) do
{:done, edits}
end
defp follow_snake(path) do
{:cont, path}
end
## Helpers
# replace_at
@@ -883,8 +662,8 @@ defmodule List do
[value | rest]
end
defp do_replace_at([head | tail], index, value) do
[head | do_replace_at(tail, index - 1, value)]
defp do_replace_at([h | t], index, value) do
[h | do_replace_at(t, index - 1, value)]
end
# insert_at
@@ -897,8 +676,8 @@ defmodule List do
[value | list]
end
defp do_insert_at([head | tail], index, value) do
[head | do_insert_at(tail, index - 1, value)]
defp do_insert_at([h | t], index, value) do
[h | do_insert_at(t, index - 1, value)]
end
# update_at
@@ -911,30 +690,30 @@ defmodule List do
list
end
defp do_update_at([head | tail], index, fun) do
[head | do_update_at(tail, index - 1, fun)]
defp do_update_at([h | t], index, fun) do
[h | do_update_at(t, index - 1, fun)]
end
defp do_update_at([], _index, _fun) do
[]
end
# pop_at
# delete_at
defp do_pop_at([], _index, default, acc) do
{default, :lists.reverse(acc)}
defp do_delete_at([], _index) do
[]
end
defp do_pop_at(list, index, default, []) when index < 0 do
{default, list}
defp do_delete_at([_ | t], 0) do
t
end
defp do_pop_at([head | tail], 0, _default, acc) do
{head, :lists.reverse(acc, tail)}
defp do_delete_at(list, index) when index < 0 do
list
end
defp do_pop_at([head | tail], index, default, acc) do
do_pop_at(tail, index - 1, default, [head | acc])
defp do_delete_at([h | t], index) do
[h | do_delete_at(t, index - 1)]
end
# zip
@@ -952,8 +731,8 @@ defmodule List do
{nil, nil}
end
defp do_zip_each([head | tail], acc) do
{tail, [head | acc]}
defp do_zip_each([h | t], acc) do
{t, [h | acc]}
end
defp do_zip_each([], _) do
+6 -12
View File
@@ -1,24 +1,18 @@
defprotocol List.Chars do
@moduledoc ~S"""
The `List.Chars` protocol is responsible for
converting a structure to a charlist (only if applicable).
The List.Chars protocol is responsible for
converting a structure to a list (only if applicable).
The only function required to be implemented is
`to_charlist/1` which does the conversion.
`to_charlist` which does the conversion.
The `to_charlist/1` function automatically imported
by `Kernel` invokes this protocol.
The `to_charlist` function automatically imported
by Kernel invokes this protocol.
"""
@doc """
Converts `term` to a charlist.
"""
@spec to_charlist(t) :: charlist
def to_charlist(term)
# TODO: Deprecate by v1.5
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
Kernel.def to_char_list(term) do
__MODULE__.to_charlist(term)
end
+63 -167
View File
@@ -56,30 +56,25 @@ defmodule Macro do
"""
@typedoc "Abstract Syntax Tree (AST)"
@type t :: expr | literal
@type t :: expr | {t, t} | atom | number | binary | pid | fun | [t]
@type expr :: {expr | atom, Keyword.t, atom | [t]}
@typedoc "Represents expressions in the AST"
@type expr :: {expr | atom, keyword, atom | [t]}
@typedoc "Represents literals in the AST"
@type literal :: atom | number | binary | fun | {t, t} | [t]
binary_ops =
[:===, :!==, :==, :!=, :<=, :>=,
:&&, :||, :<>, :++, :--, :\\, :::, :<-, :.., :|>, :=~,
:<, :>, :->,
:+, :-, :*, :/, :=, :|, :.,
:and, :or, :when, :in,
:~>>, :<<~, :~>, :<~, :<~>, :<|>,
:<<<, :>>>, :|||, :&&&, :^^^, :~~~]
@binary_ops [:===, :!==,
:==, :!=, :<=, :>=,
:&&, :||, :<>, :++, :--, :\\, :::, :<-, :.., :|>, :=~,
:<, :>, :->,
:+, :-, :*, :/, :=, :|, :.,
:and, :or, :when, :in,
:~>>, :<<~, :~>, :<~, :<~>, :<|>,
:<<<, :>>>, :|||, :&&&, :^^^, :~~~]
@doc false
defmacro binary_ops, do: unquote(binary_ops)
defmacro binary_ops, do: @binary_ops
unary_ops = [:!, :@, :^, :not, :+, :-, :~~~, :&]
@unary_ops [:!, :@, :^, :not, :+, :-, :~~~, :&]
@doc false
defmacro unary_ops, do: unquote(unary_ops)
defmacro unary_ops, do: @unary_ops
@spec binary_op_props(atom) :: {:left | :right, precedence :: integer}
defp binary_op_props(o) do
@@ -103,69 +98,6 @@ defmodule Macro do
end
end
# Classifies the given atom into one of the following categories:
#
# * :alias - a valid Elixir alias, like Foo, Foo.Bar and so on
#
# * :callable - an atom that can be used as a function call after the
# . operator (for example, :<> is callable because Foo.<>(1, 2, 3) is valid
# syntax); this category includes identifiers like :foo
#
# * :not_callable - an atom that cannot be used as a function call after the
# . operator (for example, :<<>> is not callable because Foo.<<>> is a
# syntax error); this category includes atoms like :Foo, since they are
# valid identifiers but they need quotes to be used in function calls
# (Foo."Bar")
#
# * :other - any other atom (these are usually escaped when inspected, like
# :"foo and bar")
@doc false
def classify_identifier(atom) when is_atom(atom) do
charlist = Atom.to_charlist(atom)
cond do
atom in [:"%", :"%{}", :"{}", :"<<>>", :"...", :"..", :"."] ->
:not_callable
atom in unquote(unary_ops) or atom in unquote(binary_ops) ->
:callable
valid_alias?(charlist) ->
:alias
true ->
case :elixir_config.safe_get(:identifier_tokenizer, String.Tokenizer).tokenize(charlist) do
{kind, _acc, [], _, _, special} ->
if kind == :identifier and not :lists.member(?@, special) do
:callable
else
:not_callable
end
_ ->
:other
end
end
end
defp valid_alias?('Elixir' ++ rest), do: valid_alias_piece?(rest)
defp valid_alias?(_other), do: false
defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))
defp valid_alias_piece?([]),
do: true
defp valid_alias_piece?(_other),
do: false
defp trim_leading_while_valid_identifier([char | rest])
when char >= ?a and char <= ?z
when char >= ?A and char <= ?Z
when char >= ?0 and char <= ?9
when char == ?_ do
trim_leading_while_valid_identifier(rest)
end
defp trim_leading_while_valid_identifier(other) do
other
end
@doc """
Breaks a pipeline expression into a list.
@@ -227,7 +159,7 @@ defmodule Macro do
end
def pipe(expr, {call, _, [_, _]} = call_args, _integer)
when call in unquote(binary_ops) do
when call in unquote(@binary_ops) do
raise ArgumentError, "cannot pipe #{to_string expr} into #{to_string call_args}, " <>
"the #{to_string call} operator can only take two arguments"
end
@@ -260,7 +192,7 @@ defmodule Macro do
end
@doc false
def pipe_warning({call, _, _}) when call in unquote(unary_ops) do
def pipe_warning({call, _, _}) when call in unquote(@unary_ops) do
"piping into a unary operator is deprecated. You could use e.g. Kernel.+(5) instead of +5"
end
def pipe_warning(_), do: nil
@@ -280,7 +212,7 @@ defmodule Macro do
{:sample, [], []}
"""
@spec update_meta(t, (keyword -> keyword)) :: t
@spec update_meta(t, (Keyword.t -> Keyword.t)) :: t
def update_meta(quoted, fun)
def update_meta({left, meta, right}, fun) when is_list(meta) do
@@ -291,21 +223,6 @@ defmodule Macro do
other
end
@doc """
Generates AST nodes for a given number of required argument variables using
`Macro.var/2`.
## Examples
iex> Macro.generate_arguments(2, __MODULE__)
[{:var1, [], __MODULE__}, {:var2, [], __MODULE__}]
"""
def generate_arguments(0, _), do: []
def generate_arguments(amount, context) when is_integer(amount) and amount > 0 and is_atom(context) do
for id <- 1..amount, do: Macro.var(String.to_atom("var" <> Integer.to_string(id)), context)
end
@doc """
Generates an AST node representing the variable given
by the atoms `var` and `context`.
@@ -314,7 +231,7 @@ defmodule Macro do
In order to build a variable, a context is expected.
Most of the times, in order to preserve hygiene, the
context must be `__MODULE__/0`:
context must be `__MODULE__`:
iex> Macro.var(:foo, __MODULE__)
{:foo, [], __MODULE__}
@@ -474,7 +391,8 @@ defmodule Macro do
1
"""
@spec escape(term, keyword) :: Macro.t
@spec escape(term) :: Macro.t
@spec escape(term, Keyword.t) :: Macro.t
def escape(expr, opts \\ []) do
elem(:elixir_quote.escape(expr, Keyword.get(opts, :unquote, false)), 0)
end
@@ -482,7 +400,7 @@ defmodule Macro do
@doc """
Validates the given expressions are valid quoted expressions.
Checks the `t:Macro.t/0` for the specification of a valid
Checks the `type:Macro.t` for the specification of a valid
quoted expression.
It returns `:ok` if the expression is valid. Otherwise it returns a tuple in the form of
@@ -585,8 +503,8 @@ defmodule Macro do
def unescape_map(?u), do: true
def unescape_map(e), do: e
If the `unescape_map/1` function returns `false`, the char is
not escaped and the backslash is kept in the string.
If the `unescape_map` function returns `false`. The char is
not escaped and `\` is kept in the charlist.
Hexadecimals and Unicode codepoints will be escaped if the map
function returns `true` for `?x`. Unicode codepoints if the map
@@ -594,7 +512,7 @@ defmodule Macro do
## Examples
Using the `unescape_map/1` function defined above is easy:
Using the `unescape_map` function defined above is easy:
Macro.unescape_string "example\\n", &unescape_map(&1)
@@ -633,24 +551,13 @@ defmodule Macro do
@doc """
Converts the given expression to a binary.
The given `fun` is called for every node in the AST with two arguments: the
AST of the node being printed and the string representation of that same
node. The return value of this function is used as the final string
representation for that AST node.
## Examples
iex> Macro.to_string(quote(do: foo.bar(1, 2, 3)))
"foo.bar(1, 2, 3)"
iex> Macro.to_string(quote(do: 1 + 2), fn
...> 1, _string -> "one"
...> 2, _string -> "two"
...> _ast, string -> string
...> end)
"one + two"
"""
@spec to_string(Macro.t) :: String.t
@spec to_string(Macro.t, (Macro.t, String.t -> String.t)) :: String.t
def to_string(tree, fun \\ fn(_ast, string) -> string end)
@@ -748,7 +655,7 @@ defmodule Macro do
end
# Binary ops
def to_string({op, _, [left, right]} = ast, fun) when op in unquote(binary_ops) do
def to_string({op, _, [left, right]} = ast, fun) when op in unquote(@binary_ops) do
fun.(ast, op_to_string(left, fun, op, :left) <> " #{op} " <> op_to_string(right, fun, op, :right))
end
@@ -773,14 +680,9 @@ defmodule Macro do
fun.(ast, "&(" <> to_string(arg, fun) <> ")")
end
# left not in right
def to_string({:not, _, [{:in, _, [left, right]}]} = ast, fun) do
fun.(ast, to_string(left, fun) <> " not in " <> to_string(right, fun))
end
# Unary ops
def to_string({unary, _, [{binary, _, [_, _]} = arg]} = ast, fun)
when unary in unquote(unary_ops) and binary in unquote(binary_ops) do
when unary in unquote(@unary_ops) and binary in unquote(@binary_ops) do
fun.(ast, Atom.to_string(unary) <> "(" <> to_string(arg, fun) <> ")")
end
@@ -788,13 +690,13 @@ defmodule Macro do
fun.(ast, "not " <> to_string(arg, fun))
end
def to_string({op, _, [arg]} = ast, fun) when op in unquote(unary_ops) do
def to_string({op, _, [arg]} = ast, fun) when op in unquote(@unary_ops) do
fun.(ast, Atom.to_string(op) <> to_string(arg, fun))
end
# Access
def to_string({{:., _, [Access, :get]}, _, [{op, _, _} = left, right]} = ast, fun)
when op in unquote(binary_ops) do
when op in unquote(@binary_ops) do
fun.(ast, "(" <> to_string(left, fun) <> ")" <> to_string([right], fun))
end
@@ -826,9 +728,8 @@ defmodule Macro do
list == [] ->
"[]"
:io_lib.printable_list(list) ->
{escaped, _} = Inspect.BitString.escape(IO.chardata_to_string(list), ?')
IO.iodata_to_binary [?', escaped, ?']
Inspect.List.keyword?(list) ->
"'" <> Inspect.BitString.escape(IO.chardata_to_string(list), ?') <> "'"
Keyword.keyword?(list) ->
"[" <> kw_list_to_string(list, fun) <> "]"
true ->
"[" <> Enum.map_join(list, ", ", &to_string(&1, fun)) <> "]"
@@ -850,11 +751,11 @@ defmodule Macro do
to_string(ast, fun)
end
defp bitmods_to_string({op, _, [left, right]} = ast, fun, _, _) when op in [:*, :-] do
defp bitmods_to_string({:-, _, [left, right]} = ast, fun, _, _) do
result =
bitmods_to_string(left, fun, op, :left) <>
Atom.to_string(op) <>
bitmods_to_string(right, fun, op, :right)
bitmods_to_string(left, fun, :-, :left) <>
"-" <>
bitmods_to_string(right, fun, :-, :right)
fun.(ast, result)
end
@@ -863,10 +764,10 @@ defmodule Macro do
end
# Block keywords
kw_keywords = [:do, :catch, :rescue, :after, :else]
@kw_keywords [:do, :catch, :rescue, :after, :else]
defp kw_blocks?([{:do, _} | _] = kw) do
Enum.all?(kw, &match?({x, _} when x in unquote(kw_keywords), &1))
Enum.all?(kw, &match?({x, _} when x in unquote(@kw_keywords), &1))
end
defp kw_blocks?(_), do: false
@@ -923,12 +824,8 @@ defmodule Macro do
do: Atom.to_string(atom)
defp call_to_string({:., _, [{:&, _, [val]} = arg]}, fun) when not is_integer(val),
do: "(" <> module_to_string(arg, fun) <> ")."
defp call_to_string({:., _, [{:fn, _, _} = arg]}, fun),
do: "(" <> module_to_string(arg, fun) <> ")."
defp call_to_string({:., _, [arg]}, fun),
do: module_to_string(arg, fun) <> "."
defp call_to_string({:., _, [left, right]}, fun) when is_atom(right),
do: module_to_string(left, fun) <> "." <> call_to_string_for_atom(right)
defp call_to_string({:., _, [left, right]}, fun),
do: module_to_string(left, fun) <> "." <> call_to_string(right, fun)
defp call_to_string(other, fun),
@@ -940,14 +837,10 @@ defmodule Macro do
target <> "(" <> args <> ")"
end
defp call_to_string_for_atom(atom) do
Inspect.Function.escape_name(atom)
end
defp args_to_string(args, fun) do
{list, last} = :elixir_utils.split_last(args)
if last != [] and Inspect.List.keyword?(last) do
if last != [] and Keyword.keyword?(last) do
prefix =
case list do
[] -> ""
@@ -960,7 +853,7 @@ defmodule Macro do
end
defp kw_blocks_to_string(kw, fun) do
Enum.reduce(unquote(kw_keywords), " ", fn(x, acc) ->
Enum.reduce(@kw_keywords, " ", fn(x, acc) ->
case Keyword.has_key?(kw, x) do
true -> acc <> kw_block_to_string(x, Keyword.get(kw, x), fun)
false -> acc
@@ -992,7 +885,7 @@ defmodule Macro do
defp map_to_string(list, fun) do
cond do
Inspect.List.keyword?(list) -> kw_list_to_string(list, fun)
Keyword.keyword?(list) -> kw_list_to_string(list, fun)
true -> map_list_to_string(list, fun)
end
end
@@ -1013,22 +906,22 @@ defmodule Macro do
end)
end
defp wrap_in_parenthesis(expr, fun) do
defp parenthise(expr, fun) do
"(" <> to_string(expr, fun) <> ")"
end
defp op_to_string({op, _, [_, _]} = expr, fun, parent_op, side) when op in unquote(binary_ops) do
defp op_to_string({op, _, [_, _]} = expr, fun, parent_op, side) when op in unquote(@binary_ops) do
{parent_assoc, parent_prec} = binary_op_props(parent_op)
{_, prec} = binary_op_props(op)
cond do
parent_prec < prec -> to_string(expr, fun)
parent_prec > prec -> wrap_in_parenthesis(expr, fun)
parent_prec > prec -> parenthise(expr, fun)
true ->
# parent_prec == prec, so look at associativity.
if parent_assoc == side do
to_string(expr, fun)
else
wrap_in_parenthesis(expr, fun)
parenthise(expr, fun)
end
end
end
@@ -1065,7 +958,7 @@ defmodule Macro do
* Macros (local or remote)
* Aliases are expanded (if possible) and return atoms
* Compilation environment macros (`__ENV__/0`, `__MODULE__/0` and `__DIR__/0`)
* Pseudo-variables (`__ENV__`, `__MODULE__` and `__DIR__`)
* Module attributes reader (`@foo`)
If the expression cannot be expanded, it returns the expression
@@ -1106,7 +999,7 @@ defmodule Macro do
That said, we need to expand the aliases node above to an
atom, so we can retrieve its length. Expanding the node is
not straightforward because we also need to expand the
not straight-forward because we also need to expand the
caller aliases. For example:
alias MyHelpers, as: My
@@ -1157,7 +1050,7 @@ defmodule Macro do
end
end
# Expand compilation environment macros
# Expand pseudo-variables
defp do_expand_once({:__MODULE__, _, atom}, env) when is_atom(atom),
do: {env.module, true}
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
@@ -1295,28 +1188,31 @@ defmodule Macro do
"Elixir." <> rest = Atom.to_string(atom)
underscore(rest)
end
def underscore(""), do: ""
def underscore(<<h, t::binary>>) do
<<to_lower_char(h)>> <> do_underscore(t, h)
end
def underscore("") do
""
end
defp do_underscore(<<h, t, rest::binary>>, _)
when (h >= ?A and h <= ?Z) and not (t >= ?A and t <= ?Z) and t != ?. and t != ?_ do
when (h >= ?A and h <= ?Z) and not (t >= ?A and t <= ?Z) and t != ?. do
<<?_, to_lower_char(h), t>> <> do_underscore(rest, t)
end
defp do_underscore(<<h, t::binary>>, prev)
when (h >= ?A and h <= ?Z) and not (prev >= ?A and prev <= ?Z) and prev != ?_ do
when (h >= ?A and h <= ?Z) and not (prev >= ?A and prev <= ?Z) do
<<?_, to_lower_char(h)>> <> do_underscore(t, h)
end
defp do_underscore(<<?., t::binary>>, _) do
<<?/>> <> underscore(t)
end
defp do_underscore(<<h, t::binary>>, _) do
<<to_lower_char(h)>> <> do_underscore(t, h)
end
defp do_underscore(<<>>, _) do
<<>>
end
@@ -1334,37 +1230,37 @@ defmodule Macro do
iex> Macro.camelize "foo_bar"
"FooBar"
If uppercase characters are present, they are not modified in anyway
as a mechanism to preserve acronyms:
iex> Macro.camelize "API.V1"
"API.V1"
iex> Macro.camelize "API_SPEC"
"API_SPEC"
"""
@spec camelize(String.t) :: String.t
def camelize(string)
def camelize(""),
do: ""
def camelize(<<?_, t::binary>>),
do: camelize(t)
def camelize(<<h, t::binary>>),
do: <<to_upper_char(h)>> <> do_camelize(t)
defp do_camelize(<<?_, ?_, t::binary>>),
do: do_camelize(<<?_, t::binary >>)
defp do_camelize(<<?_, h, t::binary>>) when h >= ?a and h <= ?z,
do: <<to_upper_char(h)>> <> do_camelize(t)
defp do_camelize(<<?_, h, t::binary>>) when h >= ?0 and h <= ?9,
do: <<h>> <> do_camelize(t)
defp do_camelize(<<?_>>),
do: <<>>
defp do_camelize(<<?/, t::binary>>),
do: <<?.>> <> camelize(t)
defp do_camelize(<<h, t::binary>>),
do: <<h>> <> do_camelize(t)
defp do_camelize(<<>>),
do: <<>>
+11 -25
View File
@@ -3,8 +3,8 @@ defmodule Macro.Env do
A struct that holds compile time environment information.
The current environment can be accessed at any time as
`__ENV__/0`. Inside macros, the caller environment can be
accessed as `__CALLER__/0`.
`__ENV__`. Inside macros, the caller environment can be
accessed as `__CALLER__`.
An instance of `Macro.Env` must not be modified by hand. If you need to
create a custom environment to pass to `Code.eval_quoted/3`, use the
@@ -36,20 +36,12 @@ defmodule Macro.Env do
* `macros` - a list of macros imported from each module
* `macro_aliases` - a list of aliases defined inside the current macro
* `context_modules` - a list of modules defined in the current context
* `lexical_tracker` - PID of the lexical tracker which is responsible for
keeping user info
* `vars` - a list keeping all defined variables as `{var, context}`
The following fields are private and must not be accessed or relied on:
* `export_vars` - a list keeping all variables to be exported in a
construct (may be `nil`)
* `match_vars` - controls how "new" variables are handled. Inside a
match it is a list with all variables in a match. Outside of a match
is either `:warn` or `:apply`
* `prematch_vars` - a list of variables defined before a match (is
`nil` when not inside a match)
* `lexical_tracker` - PID of the lexical tracker which is responsible for
keeping user info
* `local` - the module to expand local functions to
"""
@type name_arity :: {atom, arity}
@@ -63,13 +55,10 @@ defmodule Macro.Env do
@type macros :: [{module, [name_arity]}]
@type context_modules :: [module]
@type vars :: [{atom, atom | non_neg_integer}]
@type lexical_tracker :: pid | nil
@type export_vars :: vars | nil
@type lexical_tracker :: pid
@type local :: atom | nil
@opaque export_vars :: vars | nil
@opaque match_vars :: vars | :warn | :apply
@opaque prematch_vars :: vars | nil
@type t :: %{__struct__: __MODULE__,
module: atom,
file: file,
@@ -84,9 +73,8 @@ defmodule Macro.Env do
context_modules: context_modules,
vars: vars,
export_vars: export_vars,
match_vars: match_vars,
prematch_vars: prematch_vars,
lexical_tracker: lexical_tracker}
lexical_tracker: lexical_tracker,
local: local}
def __struct__ do
%{__struct__: __MODULE__,
@@ -102,10 +90,8 @@ defmodule Macro.Env do
macro_aliases: [],
context_modules: [],
vars: [],
lexical_tracker: nil,
export_vars: nil,
match_vars: :warn,
prematch_vars: nil}
lexical_tracker: nil}
end
def __struct__(kv) do
@@ -116,7 +102,7 @@ defmodule Macro.Env do
Returns a keyword list containing the file and line
information as keys.
"""
@spec location(t) :: keyword
@spec location(t) :: Keyword.t
def location(env)
def location(%{__struct__: Macro.Env, file: file, line: line}) do
[file: file, line: line]
+145 -351
View File
@@ -2,99 +2,15 @@ defmodule Map do
@moduledoc """
A set of functions for working with maps.
Maps are the "go to" key-value data structure in Elixir. Maps can be created
with the `%{}` syntax, and key-value pairs can be expressed as `key => value`:
iex> %{}
%{}
iex> %{"one" => :two, 3 => "four"}
%{3 => "four", "one" => :two}
Key-value pairs in a map do not follow any order (that's why the printed map
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
compared using the exact-equality operator (`===`). If colliding keys are defined
in a map literal, the last one prevails.
When the key in a key-value pair is an atom, the `key: value` shorthand syntax
can be used (as in many other special forms), provided key-value pairs are put at
the end:
iex> %{"hello" => "world", a: 1, b: 2}
%{:a => 1, :b => 2, "hello" => "world"}
Keys in maps can be accessed through some of the functions in this module
(such as `Map.get/3` or `Map.fetch/2`) or through the `[]` syntax provided by
the `Access` module:
iex> map = %{a: 1, b: 2}
iex> Map.fetch(map, :a)
{:ok, 1}
iex> map[:b]
2
iex> map["non_existing_key"]
nil
The alternative access syntax `map.key` is provided alongside `[]` when the
map has a `:key` key; note that while `map[key]` will return `nil` if `map`
doesn't contain `key`, `map.key` will raise if `map` doesn't contain
the key `:key`.
iex> map = %{foo: "bar", baz: "bong"}
iex> map.foo
"bar"
iex> map.non_existing_key
** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
Maps can be pattern matched on; when a map is on the left-hand side of a
pattern match, it will match if the map on the right-hand side contains the
keys on the left-hand side and their values match the ones on the left-hand
side. This means that an empty map matches every map.
iex> %{} = %{foo: "bar"}
%{foo: "bar"}
iex> %{a: a} = %{:a => 1, "b" => 2, [:c, :e, :e] => 3}
iex> a
1
iex> %{:c => 3} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
Variables can be used as map keys both when writing map literals as well as
when matching:
iex> n = 1
1
iex> %{n => :one}
%{1 => :one}
iex> %{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}
%{1 => :one, 2 => :two, 3 => :three}
Maps also support a specific update syntax to update the value stored under
*existing* atom keys:
iex> map = %{one: 1, two: 2}
iex> %{map | one: "one"}
%{one: "one", two: 2}
iex> %{map | three: 3}
** (KeyError) key :three not found
## Modules to work with maps
This module aims to provide functions that perform operations specific to maps
(like accessing keys, updating values, and so on). For traversing maps as
collections, developers should use the `Enum` module that works across a
variety of data types.
The `Kernel` module also provides a few functions to work with maps: for
example, `Kernel.map_size/1` to know the number of key-value pairs in a map or
`Kernel.is_map/1` to know if a term is a map.
Maps are key-value stores where keys can be any value and
are compared using the match operator (`===`). Maps can be
created with the `%{}` special form defined in the
`Kernel.SpecialForms` module.
"""
@type key :: any
@type value :: any
@compile {:inline, fetch: 2, fetch!: 2, get: 2, put: 3, delete: 2, has_key?: 2, replace!: 3}
@compile {:inline, fetch: 2, put: 3, delete: 2, has_key?: 2}
@doc """
Returns all keys from `map`.
@@ -123,9 +39,6 @@ defmodule Map do
@doc """
Converts `map` to a list.
Each key-value pair in the map is converted to a two-element tuple `{key,
value}` in the resulting list.
## Examples
iex> Map.to_list(%{a: 1})
@@ -162,9 +75,8 @@ defmodule Map do
%{a: 3}
"""
@spec new(Enumerable.t) :: map
@spec new(Enum.t) :: map
def new(enumerable)
def new(list) when is_list(list), do: :maps.from_list(list)
def new(%{__struct__: _} = struct), do: new_from_enum(struct)
def new(%{} = map), do: map
def new(enum), do: new_from_enum(enum)
@@ -176,7 +88,7 @@ defmodule Map do
end
@doc """
Creates a map from an `enumerable` via the given transformation function.
Creates a map from an `enumerable` via the transformation function.
Duplicated keys are removed; the latest one prevails.
@@ -186,25 +98,24 @@ defmodule Map do
%{a: :a, b: :b}
"""
@spec new(Enumerable.t, (term -> {key, value})) :: map
def new(enumerable, transform) when is_function(transform, 1) do
@spec new(Enum.t, (term -> {key, value})) :: map
def new(enumerable, transform) do
enumerable
|> Enum.to_list
|> new_transform(transform, [])
|> do_new_transform(transform, [])
end
defp new_transform([], _fun, acc) do
defp do_new_transform([], _fun, acc) do
acc
|> :lists.reverse
|> :maps.from_list
end
defp new_transform([item | rest], fun, acc) do
new_transform(rest, fun, [fun.(item) | acc])
defp do_new_transform([item | rest], fun, acc) do
do_new_transform(rest, fun, [fun.(item) | acc])
end
@doc """
Returns whether the given `key` exists in the given `map`.
Returns whether a given `key` exists in the given `map`.
## Examples
@@ -213,16 +124,14 @@ defmodule Map do
iex> Map.has_key?(%{a: 1}, :b)
false
Inlined by the compiler.
"""
@spec has_key?(map, key) :: boolean
def has_key?(map, key), do: :maps.is_key(key, map)
@doc """
Fetches the value for a specific `key` in the given `map`.
Fetches the value for a specific `key` and returns it in a tuple.
If `map` contains the given `key` with value `value`, then `{:ok, value}` is
returned. If `map` doesn't contain `key`, `:error` is returned.
If the `key` does not exist, returns `:error`.
## Examples
@@ -231,17 +140,14 @@ defmodule Map do
iex> Map.fetch(%{a: 1}, :b)
:error
Inlined by the compiler.
"""
@spec fetch(map, key) :: {:ok, value} | :error
def fetch(map, key), do: :maps.find(key, map)
@doc """
Fetches the value for a specific `key` in the given `map`, erroring out if
`map` doesn't contain `key`.
Fetches the value for specific `key`.
If `map` contains the given `key`, the corresponding value is returned. If
`map` doesn't contain `key`, a `KeyError` exception is raised.
If `key` does not exist, a `KeyError` is raised.
## Examples
@@ -253,82 +159,38 @@ defmodule Map do
"""
@spec fetch!(map, key) :: value | no_return
def fetch!(map, key) do
:maps.get(key, map)
case fetch(map, key) do
{:ok, value} -> value
:error -> raise KeyError, key: key, term: map
end
end
@doc """
Puts the given `value` under `key` unless the entry `key`
already exists in `map`.
already exists.
## Examples
iex> Map.put_new(%{a: 1}, :b, 2)
%{a: 1, b: 2}
%{b: 2, a: 1}
iex> Map.put_new(%{a: 1, b: 2}, :a, 3)
%{a: 1, b: 2}
"""
@spec put_new(map, key, value) :: map
def put_new(map, key, value) do
case map do
%{^key => _value} ->
map
%{} ->
put(map, key, value)
other ->
:erlang.error({:badmap, other})
case has_key?(map, key) do
true -> map
false -> put(map, key, value)
end
end
@doc """
Alters the value stored under `key` to `value`, but only
if the entry `key` already exists in `map`.
## Examples
iex> Map.replace(%{a: 1}, :b, 2)
%{a: 1}
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
"""
@spec replace(map, key, value) :: map
def replace(map, key, value) do
case map do
%{^key => _value} ->
put(map, key, value)
%{} ->
map
other ->
:erlang.error({:badmap, other})
end
end
@doc """
Similar to `replace/3`, but will raise a `KeyError`
if the key does not exist in the map.
## Examples
iex> Map.replace!(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
iex> Map.replace!(%{a: 1}, :b, 2)
** (KeyError) key :b not found in: %{a: 1}
Inlined by the compiler.
"""
@spec replace!(map, key, value) :: map
def replace!(map, key, value) do
:maps.update(key, value, map)
end
@doc """
Evaluates `fun` and puts the result under `key`
in `map` unless `key` is already present.
in map unless `key` is already present.
This function is useful in case you want to compute the value to put under
`key` only if `key` is not already present (e.g., the value is expensive to
calculate or generally difficult to setup and teardown again).
This is useful if the value is very expensive to calculate or
generally difficult to setup and teardown again.
## Examples
@@ -345,21 +207,15 @@ defmodule Map do
"""
@spec put_new_lazy(map, key, (() -> value)) :: map
def put_new_lazy(map, key, fun) when is_function(fun, 0) do
case map do
%{^key => _value} ->
map
%{} ->
put(map, key, fun.())
other ->
:erlang.error({:badmap, other})
case has_key?(map, key) do
true -> map
false -> put(map, key, fun.())
end
end
@doc """
Returns a new map with all the key-value pairs in `map` where the key
is in `keys`.
If `keys` contains keys that are not in `map`, they're simply ignored.
Takes all entries corresponding to the given keys and
returns them in a new map.
## Examples
@@ -368,38 +224,26 @@ defmodule Map do
"""
@spec take(map, Enumerable.t) :: map
def take(map, keys)
def take(map, keys) when is_map(map) do
def take(map, keys) do
keys
|> Enum.to_list
|> take(map, [])
|> do_take(map, [])
end
def take(non_map, _keys) do
:erlang.error({:badmap, non_map})
end
defp take([], _map, acc) do
:maps.from_list(acc)
end
defp take([key | rest], map, acc) do
acc =
case map do
%{^key => value} -> [{key, value} | acc]
%{} -> acc
end
take(rest, map, acc)
defp do_take([], _map, acc), do: :maps.from_list(acc)
defp do_take([key | rest], map, acc) do
acc = case fetch(map, key) do
{:ok, value} -> [{key, value} | acc]
:error -> acc
end
do_take(rest, map, acc)
end
@doc """
Gets the value for a specific `key` in `map`.
Gets the value for a specific `key`.
If `key` is present in `map` with value `value`, then `value` is
returned. Otherwise, `default` is returned (which is `nil` unless
specified otherwise).
If `key` does not exist, return the default value
(`nil` if no default value).
## Examples
@@ -413,23 +257,19 @@ defmodule Map do
3
"""
@spec get(map, key) :: value
@spec get(map, key, value) :: value
def get(map, key, default \\ nil) do
case map do
%{^key => value} ->
value
%{} ->
default
other ->
:erlang.error({:badmap, other}, [map, key, default])
case fetch(map, key) do
{:ok, value} -> value
:error -> default
end
end
@doc """
Gets the value for a specific `key` in `map`.
Gets the value for a specific `key`.
If `key` is present in `map` with value `value`, then `value` is
returned. Otherwise, `fun` is evaluated and its result is returned.
If `key` does not exist, lazily evaluates `fun` and returns its result.
This is useful if the default value is very expensive to calculate or
generally difficult to setup and teardown again.
@@ -449,18 +289,14 @@ defmodule Map do
"""
@spec get_lazy(map, key, (() -> value)) :: value
def get_lazy(map, key, fun) when is_function(fun, 0) do
case map do
%{^key => value} ->
value
%{} ->
fun.()
other ->
:erlang.error({:badmap, other}, [map, key, fun])
case fetch(map, key) do
{:ok, value} -> value
:error -> fun.()
end
end
@doc """
Puts the given `value` under `key` in `map`.
Puts the given `value` under `key`.
## Examples
@@ -469,15 +305,14 @@ defmodule Map do
iex> Map.put(%{a: 1, b: 2}, :a, 3)
%{a: 3, b: 2}
Inlined by the compiler.
"""
@spec put(map, key, value) :: map
def put(map, key, value) do
:maps.put(key, value, map)
def put(map, key, val) do
:maps.put(key, val, map)
end
@doc """
Deletes the entry in `map` for a specific `key`.
Deletes the entries in `map` for a specific `key`.
If the `key` does not exist, returns `map` unchanged.
@@ -488,7 +323,6 @@ defmodule Map do
iex> Map.delete(%{b: 2}, :a)
%{b: 2}
Inlined by the compiler.
"""
@spec delete(map, key) :: map
def delete(map, key), do: :maps.remove(key, map)
@@ -496,8 +330,7 @@ defmodule Map do
@doc """
Merges two maps into one.
All keys in `map2` will be added to `map1`, overriding any existing one
(i.e., the keys in `map2` "have precedence" over the ones in `map1`).
All keys in `map2` will be added to `map1`, overriding any existing one.
If you have a struct and you would like to merge a set of keys into the
struct, do not use this function, as it would merge all keys on the right
@@ -514,13 +347,10 @@ defmodule Map do
defdelegate merge(map1, map2), to: :maps
@doc """
Merges two maps into one, resolving conflicts through the given `callback`.
Merges two maps into one.
All keys in `map2` will be added to `map1`. The given function will be invoked
when there are duplicate keys; its arguments are `key` (the duplicate key),
`value1` (the value of `key` in `map1`), and `value2` (the value of `key` in
`map2`). The value returned by `callback` is used as the value under `key` in
the resulting map.
All keys in `map2` will be added to `map1`. The given function will
be invoked with the key, value1 and value2 to solve conflicts.
## Examples
@@ -531,25 +361,16 @@ defmodule Map do
"""
@spec merge(map, map, (key, value, value -> value)) :: map
def merge(map1, map2, callback) when is_function(callback, 3) do
if map_size(map1) > map_size(map2) do
:maps.fold fn key, val2, acc ->
update(acc, key, val2, fn val1 -> callback.(key, val1, val2) end)
end, map1, map2
else
:maps.fold fn key, val2, acc ->
update(acc, key, val2, fn val1 -> callback.(key, val2, val1) end)
end, map2, map1
end
def merge(map1, map2, callback) do
:maps.fold fn k, v2, acc ->
update(acc, k, v2, fn(v1) -> callback.(k, v1, v2) end)
end, map1, map2
end
@doc """
Updates the `key` in `map` with the given function.
If `key` is present in `map` with value `value`, `fun` is invoked with
argument `value` and its result is used as the new value of `key`. If `key` is
not present in `map`, `initial` is inserted as the value of `key`. The initial
value will not be passed through the update function.
If the `key` does not exist, inserts the given `initial` value.
## Examples
@@ -560,24 +381,18 @@ defmodule Map do
"""
@spec update(map, key, value, (value -> value)) :: map
def update(map, key, initial, fun) when is_function(fun, 1) do
case map do
%{^key => value} ->
def update(map, key, initial, fun) do
case fetch(map, key) do
{:ok, value} ->
put(map, key, fun.(value))
%{} ->
:error ->
put(map, key, initial)
other ->
:erlang.error({:badmap, other}, [map, key, initial, fun])
end
end
@doc """
Returns and removes the value associated with `key` in `map`.
If `key` is present in `map` with value `value`, `{value, new_map}` is
returned where `new_map` is the result of removing `key` from `map`. If `key`
is not present in `map`, `{default, map}` is returned.
## Examples
iex> Map.pop(%{a: 1}, :a)
@@ -591,23 +406,14 @@ defmodule Map do
@spec pop(map, key, value) :: {value, map}
def pop(map, key, default \\ nil) do
case map do
%{^key => value} ->
{value, delete(map, key)}
%{} ->
{default, map}
other ->
:erlang.error({:badmap, other}, [map, key, default])
%{^key => value} -> {value, delete(map, key)}
%{} -> {default, map}
end
end
@doc """
Lazily returns and removes the value associated with `key` in `map`.
If `key` is present in `map` with value `value`, `{value, new_map}` is
returned where `new_map` is the result of removing `key` from `map`. If `key`
is not present in `map`, `{fun_result, map}` is returned, where `fun_result`
is the result of applying `fun`.
This is useful if the default value is very expensive to calculate or
generally difficult to setup and teardown again.
@@ -626,21 +432,15 @@ defmodule Map do
"""
@spec pop_lazy(map, key, (() -> value)) :: {value, map}
def pop_lazy(map, key, fun) when is_function(fun, 0) do
case map do
%{^key => value} ->
{value, delete(map, key)}
%{} ->
{fun.(), map}
other ->
:erlang.error({:badmap, other}, [map, key, fun])
case fetch(map, key) do
{:ok, value} -> {value, delete(map, key)}
:error -> {fun.(), map}
end
end
@doc """
Drops the given `keys` from `map`.
If `keys` contains keys that are not in `map`, they're simply ignored.
## Examples
iex> Map.drop(%{a: 1, b: 2, c: 3}, [:b, :d])
@@ -648,26 +448,20 @@ defmodule Map do
"""
@spec drop(map, Enumerable.t) :: map
def drop(map, keys)
def drop(map, keys) when is_map(map) do
def drop(map, keys) do
keys
|> Enum.to_list
|> drop_list(map)
end
def drop(non_map, keys) do
:erlang.error({:badmap, non_map}, [non_map, keys])
end
defp drop_list([], acc), do: acc
defp drop_list([key | rest], acc) do
drop_list(rest, delete(acc, key))
drop_list(rest, Map.delete(acc, key))
end
@doc """
Takes all entries corresponding to the given `keys` in `map` and extracts
them into a separate map.
Takes all entries corresponding to the given `keys` and extracts them into a
separate `map`.
Returns a tuple with the new map and the old map with removed keys.
@@ -680,37 +474,28 @@ defmodule Map do
"""
@spec split(map, Enumerable.t) :: {map, map}
def split(map, keys)
def split(map, keys) when is_map(map) do
def split(map, keys) do
keys
|> Enum.to_list
|> split([], map)
|> do_split([], map)
end
def split(non_map, keys) do
:erlang.error({:badmap, non_map}, [non_map, keys])
defp do_split([], inc, exc) do
{:maps.from_list(inc), exc}
end
defp split([], included, excluded) do
{:maps.from_list(included), excluded}
end
defp split([key | rest], included, excluded) do
case excluded do
%{^key => value} ->
split(rest, [{key, value} | included], delete(excluded, key))
_other ->
split(rest, included, excluded)
defp do_split([key | rest], inc, exc) do
case fetch(exc, key) do
{:ok, value} ->
do_split(rest, [{key, value} | inc], delete(exc, key))
:error ->
do_split(rest, inc, exc)
end
end
@doc """
Updates `key` with the given function.
Updates the `key` with the given function.
If `key` is present in `map` with value `value`, `fun` is invoked with
argument `value` and its result is used as the new value of `key`. If `key` is
not present in `map`, a `KeyError` exception is raised.
If the `key` does not exist, raises `KeyError`.
## Examples
@@ -718,24 +503,30 @@ defmodule Map do
%{a: 2}
iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
** (KeyError) key :b not found in: %{a: 1}
** (KeyError) key :b not found
"""
@spec update!(map, key, (value -> value)) :: map
def update!(map, key, fun) when is_function(fun, 1) do
value = fetch!(map, key)
put(map, key, fun.(value))
@spec update!(map, key, (value -> value)) :: map | no_return
def update!(%{} = map, key, fun) do
case fetch(map, key) do
{:ok, value} ->
put(map, key, fun.(value))
:error ->
:erlang.error({:badkey, key})
end
end
def update!(map, _key, _fun), do: :erlang.error({:badmap, map})
@doc """
Gets the value from `key` and updates it, all in one pass.
`fun` is called with the current value under `key` in `map` (or `nil` if `key`
is not present in `map`) and must return a two-element tuple: the "get" value
(the retrieved value, which can be operated on before being returned) and the
new value to be stored under `key` in the resulting new map. `fun` may also
return `:pop`, which means the current value shall be removed from `map` and
returned (making this function behave like `Map.pop(map, key)`.
This `fun` argument receives the value of `key` (or `nil` if `key`
is not present) and must return a two-element tuple: the "get" value
(the retrieved value, which can be operated on before being returned)
and the new value to be stored under `key`. The `fun` may also
return `:pop`, implying the current value shall be removed
from `map` and returned.
The returned value is a tuple with the "get" value returned by
`fun` and a new map with the updated value under `key`.
@@ -760,24 +551,31 @@ defmodule Map do
"""
@spec get_and_update(map, key, (value -> {get, value} | :pop)) :: {get, map} when get: term
def get_and_update(map, key, fun) when is_function(fun, 1) do
current = get(map, key)
def get_and_update(%{} = map, key, fun) do
current =
case :maps.find(key, map) do
{:ok, value} -> value
:error -> nil
end
case fun.(current) do
{get, update} ->
{get, put(map, key, update)}
:pop ->
{current, delete(map, key)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
{get, update} -> {get, :maps.put(key, update, map)}
:pop -> {current, :maps.remove(key, map)}
end
end
def get_and_update(map, _key, _fun), do: :erlang.error({:badmap, map})
@doc """
Gets the value from `key` and updates it. Raises if there is no `key`.
Behaves exactly like `get_and_update/3`, but raises a `KeyError` exception if
`key` is not present in `map`.
This `fun` argument receives the value of `key` and must return a
two-element tuple: the "get" value (the retrieved value, which can be
operated on before being returned) and the new value to be stored under
`key`.
The returned value is a tuple with the "get" value returned by `fun` and a
new map with the updated value under `key`.
## Examples
@@ -789,7 +587,7 @@ defmodule Map do
iex> Map.get_and_update!(%{a: 1}, :b, fn current_value ->
...> {current_value, "new value!"}
...> end)
** (KeyError) key :b not found in: %{a: 1}
** (KeyError) key :b not found
iex> Map.get_and_update!(%{a: 1}, :a, fn _ ->
...> :pop
@@ -798,25 +596,25 @@ defmodule Map do
"""
@spec get_and_update!(map, key, (value -> {get, value})) :: {get, map} | no_return when get: term
def get_and_update!(map, key, fun) when is_function(fun, 1) do
value = fetch!(map, key)
case fun.(value) do
{get, update} ->
{get, put(map, key, update)}
:pop ->
{value, delete(map, key)}
other ->
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
def get_and_update!(%{} = map, key, fun) do
case :maps.find(key, map) do
{:ok, value} ->
case fun.(value) do
{get, update} -> {get, :maps.put(key, update, map)}
:pop -> {value, :maps.remove(key, map)}
end
:error ->
:erlang.error({:badkey, key})
end
end
def get_and_update!(map, _key, _fun), do: :erlang.error({:badmap, map})
@doc """
Converts a `struct` to map.
It accepts the struct module or a struct itself and
simply removes the `__struct__` field from the given struct
or from a new struct generated from the given module.
simply removes the `__struct__` field from the struct.
## Example
@@ -833,11 +631,11 @@ defmodule Map do
"""
@spec from_struct(atom | struct) :: map
def from_struct(struct) when is_atom(struct) do
delete(struct.__struct__(), :__struct__)
:maps.remove(:__struct__, struct.__struct__)
end
def from_struct(%_{} = struct) do
delete(struct, :__struct__)
def from_struct(%{__struct__: _} = struct) do
:maps.remove(:__struct__, struct)
end
@doc """
@@ -855,16 +653,12 @@ defmodule Map do
"""
@spec equal?(map, map) :: boolean
def equal?(map1, map2)
def equal?(%{} = map1, %{} = map2), do: map1 === map2
def equal?(%{} = map1, map2), do: :erlang.error({:badmap, map2}, [map1, map2])
def equal?(term, other), do: :erlang.error({:badmap, term}, [term, other])
@doc false
# TODO: Remove on 2.0
# (hard-deprecated in elixir_dispatch)
def size(map) do
IO.warn "Map.size/1 is deprecated, please use Kernel.map_size/1"
map_size(map)
end
end
+79 -118
View File
@@ -1,43 +1,17 @@
defmodule MapSet do
@moduledoc """
Functions that work on sets.
A set of functions for working with sets.
`MapSet` is the "go to" set data structure in Elixir. A set can be constructed
using `MapSet.new/0`:
iex> MapSet.new
#MapSet<[]>
A set can contain any kind of elements, and elements in a set don't have to be
of the same type. By definition, sets can't contain duplicate elements: when
inserting an element in a set where it's already present, the insertion is
simply a no-op.
iex> map_set = MapSet.new
iex> MapSet.put(map_set, "foo")
#MapSet<["foo"]>
iex> map_set |> MapSet.put("foo") |> MapSet.put("foo")
#MapSet<["foo"]>
A `MapSet` is represented internally using the `%MapSet{}` struct. This struct
can be used whenever there's a need to pattern match on something being a `MapSet`:
iex> match?(%MapSet{}, MapSet.new())
true
Note that, however, the struct fields are private and must not be accessed
directly; use the functions in this module to perform operations on sets.
`MapSet`s can also be constructed starting from other collection-type data
structures: for example, see `MapSet.new/1` or `Enum.into/2`.
The `MapSet` is represented internally as a struct,
therefore `%MapSet{}` can be used whenever there is a
need to match on any `MapSet`. Note though the struct
fields are private and must not be accessed directly.
Instead, use the functions in this module.
"""
@opaque t :: %__MODULE__{map: map}
@type value :: term
@opaque t(value) :: %__MODULE__{map: %{optional(value) => []}}
@type t :: t(term)
defstruct map: %{}, version: 2
defstruct map: %{}
@doc """
Returns a new set.
@@ -63,20 +37,18 @@ defmodule MapSet do
"""
@spec new(Enum.t) :: t
def new(enumerable)
def new(%__MODULE__{} = map_set), do: map_set
def new(%__MODULE__{} = mapset), do: mapset
def new(enumerable) do
map =
enumerable
|> Enum.to_list
|> new_from_list([])
|> do_new([])
%MapSet{map: map}
end
@doc """
Creates a set from an enumerable via the transformation function.
Creates a mapset from an enumerable via the transformation function.
## Examples
@@ -84,52 +56,55 @@ defmodule MapSet do
#MapSet<[2, 4]>
"""
@spec new(Enum.t, (term -> val)) :: t(val) when val: value
def new(enumerable, transform) when is_function(transform, 1) do
@spec new(Enum.t, (term -> term)) :: t
def new(enumerable, transform) do
map =
enumerable
|> Enum.to_list
|> new_from_list_transform(transform, [])
|> do_new_transform(transform, [])
%MapSet{map: map}
end
defp new_from_list([], acc) do
:maps.from_list(acc)
defp do_new([], acc) do
acc
|> :lists.reverse
|> :maps.from_list
end
defp do_new([item | rest], acc) do
do_new(rest, [{item, true} | acc])
end
defp new_from_list([item | rest], acc) do
new_from_list(rest, [{item, []} | acc])
defp do_new_transform([], _fun, acc) do
acc
|> :lists.reverse
|> :maps.from_list
end
defp new_from_list_transform([], _fun, acc) do
:maps.from_list(acc)
end
defp new_from_list_transform([item | rest], fun, acc) do
new_from_list_transform(rest, fun, [{fun.(item), []} | acc])
defp do_new_transform([item | rest], fun, acc) do
do_new_transform(rest, fun, [{fun.(item), true} | acc])
end
@doc """
Deletes `value` from `map_set`.
Deletes `value` from `set`.
Returns a new set which is a copy of `map_set` but without `value`.
Returns a new set which is a copy of `set` but without `value`.
## Examples
iex> map_set = MapSet.new([1, 2, 3])
iex> MapSet.delete(map_set, 4)
iex> set = MapSet.new([1, 2, 3])
iex> MapSet.delete(set, 4)
#MapSet<[1, 2, 3]>
iex> MapSet.delete(map_set, 2)
iex> MapSet.delete(set, 2)
#MapSet<[1, 3]>
"""
@spec delete(t(val1), val2) :: t(val1) when val1: value, val2: value
def delete(%MapSet{map: map} = map_set, value) do
%{map_set | map: Map.delete(map, value)}
@spec delete(t, value) :: t
def delete(%MapSet{map: map} = set, value) do
%{set | map: Map.delete(map, value)}
end
@doc """
Returns a set that is `map_set1` without the members of `map_set2`.
Returns a set that is `set1` without the members of `set2`.
## Examples
@@ -137,18 +112,15 @@ defmodule MapSet do
#MapSet<[1]>
"""
@spec difference(t(val1), t(val2)) :: t(val1) when val1: value, val2: value
def difference(map_set1, map_set2)
@spec difference(t, t) :: t
# If the first set is less than twice the size of the second map,
# it is fastest to re-accumulate items in the first set that are not
# present in the second set.
def difference(%MapSet{map: map1}, %MapSet{map: map2})
when map_size(map1) < map_size(map2) * 2 do
map =
map1
|> Map.keys
|> filter_not_in(map2)
when map_size(map1) < map_size(map2) * 2 do
map = map1
|> Map.keys
|> filter_not_in(map2)
%MapSet{map: map}
end
@@ -156,24 +128,23 @@ defmodule MapSet do
# If the second set is less than half the size of the first set, it's fastest
# to simply iterate through each item in the second set, deleting them from
# the first set.
def difference(%MapSet{map: map1} = map_set, %MapSet{map: map2}) do
%{map_set | map: Map.drop(map1, Map.keys(map2))}
def difference(%MapSet{map: map1}, %MapSet{map: map2}) do
%MapSet{map: Map.drop(map1, Map.keys(map2))}
end
defp filter_not_in(keys, map2, acc \\ [])
defp filter_not_in([], _map2, acc), do: :maps.from_list(acc)
defp filter_not_in([key | rest], map2, acc) do
acc =
if Map.has_key?(map2, key) do
acc
else
[{key, []} | acc]
end
acc = if Map.has_key?(map2, key) do
acc
else
[{key, true} | acc]
end
filter_not_in(rest, map2, acc)
end
@doc """
Checks if `map_set1` and `map_set2` have no members in common.
Checks if `set1` and `set2` have no members in common.
## Examples
@@ -216,18 +187,12 @@ defmodule MapSet do
"""
@spec equal?(t, t) :: boolean
def equal?(%MapSet{map: map1, version: version}, %MapSet{map: map2, version: version}) do
def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do
Map.equal?(map1, map2)
end
# Elixir v1.5 change the map representation, so on
# version mismatch we need to compare the keys directly.
def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do
map_size(map1) == map_size(map2) and map_subset?(Map.keys(map1), map2)
end
@doc """
Returns a set containing only members that `map_set1` and `map_set2` have in common.
Returns a set containing only members that `set1` and `set2` have in common.
## Examples
@@ -238,14 +203,15 @@ defmodule MapSet do
#MapSet<[]>
"""
@spec intersection(t(val), t(val)) :: t(val) when val: value
def intersection(%MapSet{map: map1} = map_set, %MapSet{map: map2}) do
@spec intersection(t, t) :: t
def intersection(%MapSet{map: map1}, %MapSet{map: map2}) do
{map1, map2} = order_by_size(map1, map2)
%{map_set | map: Map.take(map2, Map.keys(map1))}
%MapSet{map: Map.take(map2, Map.keys(map1))}
end
@doc """
Checks if `map_set` contains `value`.
Checks if `set` contains `value`.
## Examples
@@ -261,7 +227,7 @@ defmodule MapSet do
end
@doc """
Inserts `value` into `map_set` if `map_set` doesn't already contain it.
Inserts `value` into `set` if `set` doesn't already contain it.
## Examples
@@ -271,13 +237,13 @@ defmodule MapSet do
#MapSet<[1, 2, 3, 4]>
"""
@spec put(t(val), new_val) :: t(val | new_val) when val: value, new_val: value
def put(%MapSet{map: map} = map_set, value) do
%{map_set | map: Map.put(map, value, [])}
@spec put(t, value) :: t
def put(%MapSet{map: map} = set, value) do
%{set | map: Map.put(map, value, true)}
end
@doc """
Returns the number of elements in `map_set`.
Returns the number of elements in `set`.
## Examples
@@ -291,9 +257,9 @@ defmodule MapSet do
end
@doc """
Checks if `map_set1`'s members are all contained in `map_set2`.
Checks if `set1`'s members are all contained in `set2`.
This function checks if `map_set1` is a subset of `map_set2`.
This function checks if `set1` is a subset of `set2`.
## Examples
@@ -308,23 +274,23 @@ defmodule MapSet do
if map_size(map1) <= map_size(map2) do
map1
|> Map.keys
|> map_subset?(map2)
|> do_subset?(map2)
else
false
end
end
defp map_subset?([], _), do: true
defp map_subset?([key | rest], map2) do
defp do_subset?([], _), do: true
defp do_subset?([key | rest], map2) do
if Map.has_key?(map2, key) do
map_subset?(rest, map2)
do_subset?(rest, map2)
else
false
end
end
@doc """
Converts `map_set` to a list.
Converts `set` to a list.
## Examples
@@ -332,13 +298,13 @@ defmodule MapSet do
[1, 2, 3]
"""
@spec to_list(t(val)) :: [val] when val: value
@spec to_list(t) :: list
def to_list(%MapSet{map: map}) do
Map.keys(map)
end
@doc """
Returns a set containing all members of `map_set1` and `map_set2`.
Returns a set containing all members of `set1` and `set2`.
## Examples
@@ -346,30 +312,25 @@ defmodule MapSet do
#MapSet<[1, 2, 3, 4]>
"""
@spec union(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value
def union(map_set1, map_set2)
def union(%MapSet{map: map1, version: version} = map_set, %MapSet{map: map2, version: version}) do
%{map_set | map: Map.merge(map1, map2)}
end
@spec union(t, t) :: t
def union(%MapSet{map: map1}, %MapSet{map: map2}) do
new_from_list(Map.keys(map1) ++ Map.keys(map2), [])
%MapSet{map: Map.merge(map1, map2)}
end
defp order_by_size(map1, map2) when map_size(map1) > map_size(map2), do: {map2, map1}
defp order_by_size(map1, map2), do: {map1, map2}
defimpl Enumerable do
def reduce(map_set, acc, fun), do: Enumerable.List.reduce(MapSet.to_list(map_set), acc, fun)
def member?(map_set, val), do: {:ok, MapSet.member?(map_set, val)}
def count(map_set), do: {:ok, MapSet.size(map_set)}
def reduce(set, acc, fun), do: Enumerable.List.reduce(MapSet.to_list(set), acc, fun)
def member?(set, val), do: {:ok, MapSet.member?(set, val)}
def count(set), do: {:ok, MapSet.size(set)}
end
defimpl Collectable do
def into(original) do
{original, fn
map_set, {:cont, x} -> MapSet.put(map_set, x)
map_set, :done -> map_set
set, {:cont, x} -> MapSet.put(set, x)
set, :done -> set
_, :halt -> :ok
end}
end
@@ -378,8 +339,8 @@ defmodule MapSet do
defimpl Inspect do
import Inspect.Algebra
def inspect(map_set, opts) do
concat ["#MapSet<", Inspect.List.inspect(MapSet.to_list(map_set), opts), ">"]
def inspect(set, opts) do
concat ["#MapSet<", Inspect.List.inspect(MapSet.to_list(set), opts), ">"]
end
end
end
+398 -723
View File
File diff suppressed because it is too large Load Diff
+104 -94
View File
@@ -65,15 +65,11 @@ defmodule Module.LocalsTracker do
"""
@spec reachable(ref) :: [local]
def reachable(ref) do
ref
|> to_pid()
|> :gen_server.call(:digraph, @timeout)
|> reachable_from(:local)
|> :sets.to_list()
reachable_from(:gen_server.call(to_pid(ref), :digraph, @timeout), :local)
end
defp reachable_from(d, starting) do
reduce_reachable(d, starting, :sets.new)
:sets.to_list(reduce_reachable(d, starting, :sets.new))
end
defp reduce_reachable(d, vertex, vertices) do
@@ -87,12 +83,13 @@ defmodule Module.LocalsTracker do
defp to_pid(pid) when is_pid(pid), do: pid
defp to_pid(mod) when is_atom(mod) do
table = :elixir_module.data_table(mod)
:ets.lookup_element(table, {:elixir, :locals_tracker}, 2)
[{_, val}] = :ets.lookup(table, {:elixir, :locals_tracker})
val
end
# Internal API
# Starts the tracker and returns its PID.
# Starts the tracker and returns its pid.
@doc false
def start_link do
:gen_server.start_link(__MODULE__, [], [])
@@ -138,8 +135,8 @@ defmodule Module.LocalsTracker do
# Reattach a previously yanked node
@doc false
def reattach(pid, tuple, kind, function, neighbours) do
:gen_server.cast(to_pid(pid), {:reattach, tuple, kind, function, neighbours})
def reattach(pid, kind, tuple, neighbours) do
:gen_server.cast(to_pid(pid), {:reattach, kind, tuple, neighbours})
end
# Collecting all conflicting imports with the given functions
@@ -147,11 +144,11 @@ defmodule Module.LocalsTracker do
def collect_imports_conflicts(pid, all_defined) do
d = :gen_server.call(pid, :digraph, @timeout)
for {{name, arity}, _, meta, _} <- all_defined,
for {name, arity} <- all_defined,
:digraph.in_neighbours(d, {:import, name, arity}) != [],
n = :digraph.out_neighbours(d, {:import, name, arity}),
n != [] do
{meta, {n, name, arity}}
{n, name, arity}
end
end
@@ -161,63 +158,73 @@ defmodule Module.LocalsTracker do
@doc false
def collect_unused_locals(ref, private) do
d = :gen_server.call(to_pid(ref), :digraph, @timeout)
reachable = reachable_from(d, :local)
reattached = :digraph.out_neighbours(d, :reattach)
{unreachable(reachable, reattached, private), collect_warnings(reachable, private)}
{unreachable(d, private), collect_warnings(d, private)}
end
defp unreachable(reachable, reattached, private) do
for {tuple, kind, _, _} <- private,
not reachable?(tuple, kind, reachable, reattached),
do: tuple
defp unreachable(d, private) do
unreachable = for {tuple, _, _} <- private, do: tuple
private =
for {tuple, :defp, _} <- private do
neighbours = :digraph.in_neighbours(d, tuple)
neighbours = for {_, _} = t <- neighbours, do: t
{tuple, :sets.from_list(neighbours)}
end
reduce_unreachable(private, [], :sets.from_list(unreachable))
end
defp reachable?(tuple, :defmacrop, reachable, reattached) do
# All private micros are unreachable unless they have been
# reattached and they are reachable.
:lists.member(tuple, reattached) and :sets.is_element(tuple, reachable)
end
defp reachable?(tuple, :defp, reachable, _reattached) do
:sets.is_element(tuple, reachable)
end
defp collect_warnings(reachable, private) do
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
end
defp collect_warnings({_, _, false, _}, acc, _reachable) do
acc
end
defp collect_warnings({tuple, kind, meta, 0}, acc, reachable) do
if :sets.is_element(tuple, reachable) do
acc
defp reduce_unreachable([{vertex, callers} | t], acc, unreachable) do
if :sets.is_subset(callers, unreachable) do
reduce_unreachable(t, [{vertex, callers} | acc], unreachable)
else
[{meta, {:unused_def, tuple, kind}} | acc]
reduce_unreachable(acc ++ t, [], :sets.del_element(vertex, unreachable))
end
end
defp collect_warnings({tuple, kind, meta, default}, acc, reachable) when default > 0 do
defp reduce_unreachable([], _acc, unreachable) do
:sets.to_list(unreachable)
end
defp collect_warnings(d, private) do
reachable = reachable_from(d, :local)
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
end
defp collect_warnings({tuple, kind, 0}, acc, reachable) do
if :lists.member(tuple, reachable) do
acc
else
[{:unused_def, tuple, kind} | acc]
end
end
defp collect_warnings({tuple, kind, default}, acc, reachable) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, reachable) do
:none -> [{meta, {:unused_def, tuple, kind}} | acc]
^min -> acc
^max -> [{meta, {:unused_args, tuple}} | acc]
diff -> [{meta, {:unused_args, tuple, diff}} | acc]
invoked = for {n, a} <- reachable, n == name, a in min..max, do: a
if invoked == [] do
[{:unused_def, tuple, kind} | acc]
else
case :lists.min(invoked) - min do
0 -> acc
^default -> [{:unused_args, tuple} | acc]
unused_args -> [{:unused_args, tuple, unused_args} | acc]
end
end
end
defp min_reachable_default(max, min, last, name, reachable) when max >= min do
case :sets.is_element({name, max}, reachable) do
true -> min_reachable_default(max - 1, min, max, name, reachable)
false -> min_reachable_default(max - 1, min, last, name, reachable)
end
@doc false
def cache_env(pid, env) do
:gen_server.call(pid, {:cache_env, env}, @timeout)
end
defp min_reachable_default(_max, _min, last, _name, _reachable) do
last
@doc false
def get_cached_env(pid, ref) do
:gen_server.call(pid, {:get_cached_env, ref}, @timeout)
end
# Stops the gen server
@@ -231,76 +238,79 @@ defmodule Module.LocalsTracker do
def init([]) do
d = :digraph.new([:protected])
:digraph.add_vertex(d, :local)
:digraph.add_vertex(d, :reattach)
{:ok, d}
end
def handle_call({:yank, local}, _from, d) do
out_vertices = :digraph.out_neighbours(d, local)
:digraph.del_edges(d, :digraph.out_edges(d, local))
{:reply, {[], out_vertices}, d}
end
def handle_call(:digraph, _from, d) do
{:reply, d, d}
{:ok, {d, []}}
end
@doc false
def handle_info(_msg, d) do
{:noreply, d}
def handle_call({:cache_env, env}, _from, {d, cache}) do
case cache do
[{i, ^env} | _] ->
{:reply, i, {d, cache}}
t ->
i = length(t)
{:reply, i, {d, [{i, env} | t]}}
end
end
def handle_cast({:add_local, from, to}, d) do
def handle_call({:get_cached_env, ref}, _from, {_, cache} = state) do
{^ref, env} = :lists.keyfind(ref, 1, cache)
{:reply, env, state}
end
def handle_call({:yank, local}, _from, {d, _} = state) do
out_vertices = :digraph.out_neighbours(d, local)
:digraph.del_edges(d, :digraph.out_edges(d, local))
{:reply, {[], out_vertices}, state}
end
def handle_call(:digraph, _from, {d, _} = state) do
{:reply, d, state}
end
@doc false
def handle_info(_msg, state) do
{:noreply, state}
end
def handle_cast({:add_local, from, to}, {d, _} = state) do
handle_add_local(d, from, to)
{:noreply, d}
{:noreply, state}
end
def handle_cast({:add_import, function, module, {name, arity}}, d) do
def handle_cast({:add_import, function, module, {name, arity}}, {d, _} = state) do
handle_import(d, function, module, name, arity)
{:noreply, d}
{:noreply, state}
end
def handle_cast({:add_definition, kind, tuple}, d) do
def handle_cast({:add_definition, kind, tuple}, {d, _} = state) do
handle_add_definition(d, kind, tuple)
{:noreply, d}
{:noreply, state}
end
def handle_cast({:add_defaults, kind, {name, arity}, defaults}, d) do
def handle_cast({:add_defaults, kind, {name, arity}, defaults}, {d, _} = state) do
for i <- :lists.seq(arity - defaults, arity - 1) do
handle_add_definition(d, kind, {name, i})
handle_add_local(d, {name, i}, {name, arity})
handle_add_local(d, {name, i}, {name, i + 1})
end
{:noreply, d}
{:noreply, state}
end
def handle_cast({:reattach, tuple, kind, function, {in_neigh, out_neigh}}, d) do
# Reattach the old function
def handle_cast({:reattach, _kind, tuple, {in_neigh, out_neigh}}, {d, _} = state) do
for from <- in_neigh do
:digraph.add_vertex(d, from)
replace_edge!(d, from, function)
replace_edge!(d, from, tuple)
end
for to <- out_neigh do
:digraph.add_vertex(d, to)
replace_edge!(d, function, to)
replace_edge!(d, tuple, to)
end
# Add the new definition
handle_add_definition(d, kind, tuple)
# Make a call from the old function to the new one
if function != tuple do
handle_add_local(d, function, tuple)
end
# Finally marked the new one as reattached
replace_edge!(d, :reattach, tuple)
{:noreply, d}
{:noreply, state}
end
def handle_cast(:stop, d) do
{:stop, :normal, d}
def handle_cast(:stop, state) do
{:stop, :normal, state}
end
@doc false
+13 -19
View File
@@ -89,9 +89,6 @@ defmodule Node do
For more information, see
[`:erlang.monitor_node/2`](http://www.erlang.org/doc/man/erlang.html#monitor_node-2).
For monitoring status changes of all nodes, see
[`:net_kernel.monitor_nodes/3`](http://www.erlang.org/doc/man/net_kernel.html#monitor_nodes-2).
"""
@spec monitor(t, boolean) :: true
def monitor(node, flag) do
@@ -104,9 +101,6 @@ defmodule Node do
For more information, see
[`:erlang.monitor_node/3`](http://www.erlang.org/doc/man/erlang.html#monitor_node-3).
For monitoring status changes of all nodes, see
[`:net_kernel.monitor_nodes/3`](http://www.erlang.org/doc/man/net_kernel.html#monitor_nodes-2).
"""
@spec monitor(t, boolean, [:allow_passive_connect]) :: true
def monitor(node, flag, options) do
@@ -152,7 +146,7 @@ defmodule Node do
`:ignored` if the local node is not alive.
For more information, see
[`:net_kernel.connect_node/1`](http://www.erlang.org/doc/man/net_kernel.html#connect_node-1).
[`:erlang.connect_node/1`](http://www.erlang.org/doc/man/net_kernel.html#connect_node-1).
"""
@spec connect(t) :: boolean | :ignored
def connect(node) do
@@ -160,8 +154,8 @@ defmodule Node do
end
@doc """
Returns the PID of a new process started by the application of `fun`
on `node`. If `node` does not exist, a useless PID is returned.
Returns the pid of a new process started by the application of `fun`
on `node`. If `node` does not exist, a useless pid is returned.
For the list of available options, see
[`:erlang.spawn/2`](http://www.erlang.org/doc/man/erlang.html#spawn-2).
@@ -174,10 +168,10 @@ defmodule Node do
end
@doc """
Returns the PID of a new process started by the application of `fun`
Returns the pid of a new process started by the application of `fun`
on `node`.
If `node` does not exist, a useless PID is returned.
If `node` does not exist, a useless pid is returned.
For the list of available options, see
[`:erlang.spawn_opt/3`](http://www.erlang.org/doc/man/erlang.html#spawn_opt-3).
@@ -190,10 +184,10 @@ defmodule Node do
end
@doc """
Returns the PID of a new process started by the application of
Returns the pid of a new process started by the application of
`module.function(args)` on `node`.
If `node` does not exist, a useless PID is returned.
If `node` does not exist, a useless pid is returned.
For the list of available options, see
[`:erlang.spawn/4`](http://www.erlang.org/doc/man/erlang.html#spawn-4).
@@ -206,10 +200,10 @@ defmodule Node do
end
@doc """
Returns the PID of a new process started by the application of
Returns the pid of a new process started by the application of
`module.function(args)` on `node`.
If `node` does not exist, a useless PID is returned.
If `node` does not exist, a useless pid is returned.
For the list of available options, see
[`:erlang.spawn/5`](http://www.erlang.org/doc/man/erlang.html#spawn_opt-5).
@@ -222,10 +216,10 @@ defmodule Node do
end
@doc """
Returns the PID of a new linked process started by the application of `fun` on `node`.
Returns the pid of a new linked process started by the application of `fun` on `node`.
A link is created between the calling process and the new process, atomically.
If `node` does not exist, a useless PID is returned (and due to the link, an exit
If `node` does not exist, a useless pid is returned (and due to the link, an exit
signal with exit reason `:noconnection` will be received).
Inlined by the compiler.
@@ -236,11 +230,11 @@ defmodule Node do
end
@doc """
Returns the PID of a new linked process started by the application of
Returns the pid of a new linked process started by the application of
`module.function(args)` on `node`.
A link is created between the calling process and the new process, atomically.
If `node` does not exist, a useless PID is returned (and due to the link, an exit
If `node` does not exist, a useless pid is returned (and due to the link, an exit
signal with exit reason `:noconnection` will be received).
Inlined by the compiler.
+96 -205
View File
@@ -4,16 +4,16 @@ defmodule OptionParser do
"""
@type argv :: [String.t]
@type parsed :: keyword
@type parsed :: Keyword.t
@type errors :: [{String.t, String.t | nil}]
@type options :: [switches: keyword, strict: keyword, aliases: keyword]
@type options :: [switches: Keyword.t, strict: Keyword.t, aliases: Keyword.t]
defmodule ParseError do
defexception [:message]
end
@doc """
Parses `argv` into a keyword list.
Parses `argv` into a keywords list.
It returns a three-element tuple with the form `{parsed, args, invalid}`, where:
@@ -30,33 +30,35 @@ defmodule OptionParser do
Elixir converts switches to underscored atoms, so `--source-path` becomes
`:source_path`. This is done to better suit Elixir conventions. However, this
means that switches can't contain underscores and switches that do contain
underscores are always returned in the list of invalid switches.
underscores are always returned in the list of invalid options.
When parsing, it is common to list switches and their expected types:
Without any options, this function will try to parse all switches in the `argv`.
iex> OptionParser.parse(["--debug"], switches: [debug: :boolean])
iex> OptionParser.parse(["--debug"])
{[debug: true], [], []}
iex> OptionParser.parse(["--source", "lib"], switches: [source: :string])
iex> OptionParser.parse(["--source", "lib"])
{[source: "lib"], [], []}
iex> OptionParser.parse(["--source-path", "lib", "test/enum_test.exs", "--verbose"],
...> switches: [source_path: :string, verbose: :boolean])
iex> OptionParser.parse(["--source-path", "lib", "test/enum_test.exs", "--verbose"])
{[source_path: "lib", verbose: true], ["test/enum_test.exs"], []}
We will explore the valid switches and operation modes of option parser below.
Switches followed by a value will be assigned the value, as a string.
Switches without an argument, like `--debug` in the examples above, will
automatically be set to `true`.
## Options
The following options are supported:
* `:switches` or `:strict` - see the "Switch definitions" section below
* `:allow_nonexistent_atoms` - see the "Parsing dynamic switches" section below
* `:aliases` - see the "Aliases" section below
## Switch definitions
Switches can be specified via one of two options:
Often it is better to explicitly list the known
switches and their formats. The switches can be specified via one of two
options:
* `:switches` - defines some switches and their types. This function
still attempts to parse switches that are not in this list.
@@ -68,7 +70,7 @@ defmodule OptionParser do
specifies the type for the value of this switch (see the "Types" section below
for the possible types and more information about type casting).
Note that you should only supply the `:switches` or the`:strict` option.
Note that you should only supply the `:switches` or `:strict` option.
If you supply both, an `ArgumentError` exception will be raised.
### Types
@@ -87,20 +89,20 @@ defmodule OptionParser do
* `:float` - parses the value as a float
* `:string` - parses the value as a string
If a switch can't be parsed according to the given type, it is
returned in the invalid options list.
If a switch can't be parsed according to the given type, it is returned
in the invalid options list.
### Modifiers
Switches can be specified with modifiers, which change how
they behave. The following modifiers are supported:
* `:keep` - keeps duplicated items instead of overriding them;
works with all types except `:count`. Specifying `switch_name: :keep`
assumes the type of `:switch_name` will be `:string`.
* `:keep` - keeps duplicated items instead of overriding them; works with
all types except `:count`. Specifying `switch_name: :keep` assumes the
type of `:switch_name` will be `:string`.
To use `:keep` with a type other than `:string`, use a list as the type
for the switch. For example: `[foo: [:integer, :keep]]`.
Note that if you want to use `:keep` with a type other than `:string`, use a list
as the type for the switch. For example: `[foo: [:integer, :keep]]`.
### Negation switches
@@ -110,46 +112,6 @@ defmodule OptionParser do
iex> OptionParser.parse(["--no-op", "path/to/file"], switches: [op: :boolean])
{[op: false], ["path/to/file"], []}
### Parsing dynamic switches
`OptionParser` also includes a dynamic mode where it will attempt to parse
switches dynamically. Such can be done by not specifying the `:switches` or
`:strict` option.
iex> OptionParser.parse(["--debug"])
{[debug: true], [], []}
Switches followed by a value will be assigned the value, as a string. Switches
without an argument, like `--debug` in the examples above, will automatically be
set to `true`.
Since Elixir converts switches to atoms, the dynamic mode will only parse
switches that translate to atoms used by the runtime. Therefore, the code below
likely won't parse the given option since the `:option_parser_example` atom is
never used anywhere:
OptionParser.parse(["--option-parser-example"])
# The :option_parser_example atom is not used anywhere below
However, the code below does since the `:option_parser_example` atom is used
at some point later (or earlier) on:
{opts, _, _} = OptionParser.parse(["--option-parser-example"])
opts[:option_parser_example]
In other words, when using dynamic mode, Elixir will do the correct thing and
only parse options that are used by the runtime, ignoring all others. If you
would like to parse all switches, regardless if they exist or not, you can
force creation of atoms by passing `allow_nonexistent_atoms: true` as option.
Such option is useful when you are building command-line applications that
receive dynamically-named arguments but must be used with care on long-running
systems.
Switches followed by a value will be assigned the value, as a string.
Switches without an argument, like `--debug` in the examples above, will
automatically be set to `true`.
## Aliases
A set of aliases can be specified in the `:aliases` option:
@@ -193,7 +155,7 @@ defmodule OptionParser do
"""
@spec parse(argv, options) :: {parsed, argv, errors}
def parse(argv, opts \\ []) when is_list(argv) and is_list(opts) do
do_parse(argv, build_config(opts), [], [], [], true)
do_parse(argv, compile_config(opts), [], [], [], true)
end
@doc """
@@ -241,18 +203,16 @@ defmodule OptionParser do
## Example
iex> OptionParser.parse_head(["--source", "lib", "test/enum_test.exs", "--verbose"],
...> switches: [source: :string, verbose: :boolean])
iex> OptionParser.parse_head(["--source", "lib", "test/enum_test.exs", "--verbose"])
{[source: "lib"], ["test/enum_test.exs", "--verbose"], []}
iex> OptionParser.parse_head(["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
...> switches: [source: :string, verbose: :boolean, unlock: :boolean])
iex> OptionParser.parse_head(["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"])
{[verbose: true, source: "lib"], ["test/enum_test.exs", "--unlock"], []}
"""
@spec parse_head(argv, options) :: {parsed, argv, errors}
def parse_head(argv, opts \\ []) when is_list(argv) and is_list(opts) do
do_parse(argv, build_config(opts), [], [], [], false)
do_parse(argv, compile_config(opts), [], [], [], false)
end
@doc """
@@ -266,12 +226,10 @@ defmodule OptionParser do
## Examples
iex> OptionParser.parse_head!(["--source", "lib", "path/to/file", "--verbose"],
...> switches: [source: :string, verbose: :boolean])
iex> OptionParser.parse_head!(["--source", "lib", "path/to/file", "--verbose"])
{[source: "lib"], ["path/to/file", "--verbose"]}
iex> OptionParser.parse_head!(["--number", "lib", "test/enum_test.exs", "--verbose"],
...> strict: [number: :integer])
iex> OptionParser.parse_head!(["--number", "lib", "test/enum_test.exs", "--verbose"], strict: [number: :integer])
** (OptionParser.ParseError) 1 error found!
--number : Expected type integer, got "lib"
@@ -293,12 +251,12 @@ defmodule OptionParser do
{Enum.reverse(opts), Enum.reverse(args), Enum.reverse(invalid)}
end
defp do_parse(argv, %{switches: switches} = config, opts, args, invalid, all?) do
case next_with_config(argv, config) do
defp do_parse(argv, {aliases, switches, strict}=config, opts, args, invalid, all?) do
case next(argv, aliases, switches, strict) do
{:ok, option, value, rest} ->
# the option exists and it was successfully parsed
kinds = List.wrap Keyword.get(switches, option)
new_opts = store_option(opts, option, value, kinds)
new_opts = do_store_option(opts, option, value, kinds)
do_parse(rest, config, new_opts, args, invalid, all?)
{:invalid, option, value, rest} ->
@@ -341,6 +299,7 @@ defmodule OptionParser do
* `{:error, rest}` - there are no switches at the head of the given `argv`
"""
@spec next(argv, options) ::
{:ok, key :: atom, value :: term, argv} |
{:invalid, String.t, String.t | nil, argv} |
@@ -348,129 +307,83 @@ defmodule OptionParser do
{:error, argv}
def next(argv, opts \\ []) when is_list(argv) and is_list(opts) do
next_with_config(argv, build_config(opts))
{aliases, switches, strict} = compile_config(opts)
next(argv, aliases, switches, strict)
end
defp next_with_config([], _config) do
defp next([], _aliases, _switches, _strict) do
{:error, []}
end
defp next_with_config(["--" | _] = argv, _config) do
defp next(["--" | _] = argv, _aliases, _switches, _strict) do
{:error, argv}
end
defp next_with_config(["-" | _] = argv, _config) do
defp next(["-" | _] = argv, _aliases, _switches, _strict) do
{:error, argv}
end
defp next_with_config(["- " <> _ | _] = argv, _config) do
defp next(["- " <> _ | _] = argv, _aliases, _switches, _strict) do
{:error, argv}
end
# Handles --foo or --foo=bar
defp next_with_config(["--" <> option | rest], config) do
{option, value} = split_option(option)
tagged = tag_option(option, config)
next_tagged(tagged, value, "--" <> option, rest, config)
end
# Handles -a, -abc, -abc=something
defp next_with_config(["-" <> option | rest] = argv, config) do
%{aliases: aliases, allow_nonexistent_atoms?: allow_nonexistent_atoms?} = config
defp next(["-" <> option | rest] = argv, aliases, switches, strict) do
{option, value} = split_option(option)
original = "-" <> option
tagged = tag_option(option, switches, aliases)
cond do
is_nil(value) and negative_number?(original) ->
negative_number?(original) ->
{:error, argv}
String.contains?(option, ["-", "_"]) ->
strict and not option_defined?(tagged, switches) ->
{:undefined, original, value, rest}
String.length(option) > 1 ->
key = get_option_key(option, allow_nonexistent_atoms?)
option_key = aliases[key]
if key && option_key do
IO.warn "multi-letter aliases are deprecated, got: #{inspect(key)}"
next_tagged({:default, option_key}, value, original, rest, config)
else
next_with_config(expand_multiletter_alias(option, value) ++ rest, config)
end
true ->
# We have a regular one-letter alias here
tagged = tag_oneletter_alias(option, config)
next_tagged(tagged, value, original, rest, config)
{option, kinds, value} = normalize_option(tagged, value, switches)
{value, kinds, rest} = normalize_value(value, kinds, rest, strict)
case validate_option(value, kinds) do
{:ok, new_value} -> {:ok, option, new_value, rest}
:invalid -> {:invalid, original, value, rest}
end
end
end
defp next_with_config(argv, _config) do
defp next(argv, _aliases, _switches, _strict) do
{:error, argv}
end
defp next_tagged(tagged, value, original, rest, %{switches: switches, strict?: strict?}) do
if strict? and not option_defined?(tagged, switches) do
{:undefined, original, value, rest}
else
{option, kinds, value} = normalize_option(tagged, value, switches)
{value, kinds, rest} = normalize_value(value, kinds, rest, strict?)
case validate_option(value, kinds) do
{:ok, new_value} -> {:ok, option, new_value, rest}
:invalid -> {:invalid, original, value, rest}
end
end
end
@doc """
Receives a key-value enumerable and converts it to `t:argv/0`.
Receives a key-value enumerable and converts it to argv.
Keys must be atoms. Keys with `nil` value are discarded,
boolean values are converted to `--key` or `--no-key`
(if the value is `true` or `false`, respectively),
and all other values are converted using `Kernel.to_string/1`.
It is advised to pass to `to_argv/2` the same set of `options`
given to `parse/2`. Some switches can only be reconstructed
correctly with the `switches` information in hand.
and all other values are converted using `to_string/1`.
## Examples
iex> OptionParser.to_argv([foo_bar: "baz"])
["--foo-bar", "baz"]
iex> OptionParser.to_argv([bool: true, bool: false, discarded: nil])
["--bool", "--no-bool"]
Some switches will output different values based on the switches
flag:
iex> OptionParser.to_argv([number: 2], switches: [])
["--number", "2"]
iex> OptionParser.to_argv([number: 2], switches: [number: :count])
["--number", "--number"]
"""
@spec to_argv(Enumerable.t, options) :: argv
def to_argv(enum, opts \\ []) do
switches = Keyword.get(opts, :switches, [])
@spec to_argv(Enumerable.t) :: argv
def to_argv(enum) do
Enum.flat_map(enum, fn
{_key, nil} -> []
{key, true} -> [to_switch(key)]
{key, false} -> [to_switch(key, "--no-")]
{key, value} -> to_argv(key, value, switches)
{key, value} -> [to_switch(key), to_string(value)]
end)
end
defp to_argv(key, value, switches) do
if switches[key] == :count do
List.duplicate(to_switch(key), value)
else
[to_switch(key), to_string(value)]
end
end
defp to_switch(key, prefix \\ "--") when is_atom(key) do
prefix <> String.replace(Atom.to_string(key), "_", "-")
end
@doc ~S"""
Splits a string into `t:argv/0` chunks.
Splits a string into argv chunks.
This function splits the given `string` into a list of strings in a similar
way to many shells.
@@ -485,7 +398,7 @@ defmodule OptionParser do
"""
@spec split(String.t) :: argv
def split(string) when is_binary(string) do
def split(string) do
do_split(String.trim_leading(string, " "), "", [], nil)
end
@@ -528,24 +441,21 @@ defmodule OptionParser do
## Helpers
defp build_config(opts) do
{switches, strict?} = cond do
defp compile_config(opts) do
aliases = opts[:aliases] || []
{switches, strict} = cond do
opts[:switches] && opts[:strict] ->
raise ArgumentError, ":switches and :strict cannot be given together"
switches = opts[:switches] ->
{switches, false}
strict = opts[:strict] ->
{strict, true}
s = opts[:switches] ->
{s, false}
s = opts[:strict] ->
{s, true}
true ->
{[], false}
end
%{
aliases: opts[:aliases] || [],
allow_nonexistent_atoms?: opts[:allow_nonexistent_atoms] || false,
strict?: strict?,
switches: switches
}
{aliases, switches, strict}
end
defp validate_option(value, kinds) do
@@ -585,7 +495,7 @@ defmodule OptionParser do
end
end
defp store_option(dict, option, value, kinds) do
defp do_store_option(dict, option, value, kinds) do
cond do
:count in kinds ->
Keyword.update(dict, option, value, & &1 + 1)
@@ -596,42 +506,34 @@ defmodule OptionParser do
end
end
defp tag_option("no-" <> option = original, %{switches: switches, allow_nonexistent_atoms?: allow_nonexistent_atoms?}) do
defp tag_option("-no-" <> option, switches, _aliases) do
cond do
(negated = get_option_key(option, allow_nonexistent_atoms?)) && :boolean in List.wrap(switches[negated]) ->
(negated = get_option(option)) && :boolean in List.wrap(switches[negated]) ->
{:negated, negated}
option_key = get_option_key(original, allow_nonexistent_atoms?) ->
{:default, option_key}
option = get_option("no-" <> option) ->
{:default, option}
true ->
:unknown
end
end
defp tag_option(option, %{allow_nonexistent_atoms?: allow_nonexistent_atoms?}) do
if option_key = get_option_key(option, allow_nonexistent_atoms?) do
{:default, option_key}
defp tag_option("-" <> option, _switches, _aliases) do
if option = get_option(option) do
{:default, option}
else
:unknown
end
end
defp tag_oneletter_alias(alias, %{aliases: aliases, allow_nonexistent_atoms?: allow_nonexistent_atoms?}) when is_binary(alias) do
if option_key = aliases[to_existing_key(alias, allow_nonexistent_atoms?)] do
{:default, option_key}
defp tag_option(option, _switches, aliases) when is_binary(option) do
opt = get_option(option)
if alias = aliases[opt] do
{:default, alias}
else
:unknown
end
end
defp expand_multiletter_alias(letters, value) when is_binary(letters) do
{last, expanded} =
letters
|> String.codepoints()
|> Enum.map(&("-" <> &1))
|> List.pop_at(-1)
expanded ++ [last <> if(value, do: "=" <> value, else: "")]
end
defp option_defined?(:unknown, _switches) do
false
end
@@ -660,7 +562,7 @@ defmodule OptionParser do
{option, List.wrap(switches[option]), value}
end
defp normalize_value(nil, kinds, t, strict?) do
defp normalize_value(nil, kinds, t, strict) do
cond do
:boolean in kinds ->
{true, kinds, t}
@@ -669,7 +571,7 @@ defmodule OptionParser do
value_in_tail?(t) ->
[h | t] = t
{h, kinds, t}
kinds == [] and strict? ->
kinds == [] and strict ->
{nil, kinds, t}
kinds == [] ->
{true, kinds, t}
@@ -678,7 +580,7 @@ defmodule OptionParser do
end
end
defp normalize_value(value, kinds, t, _strict?) do
defp normalize_value(value, kinds, t, _) do
{value, kinds, t}
end
@@ -706,19 +608,9 @@ defmodule OptionParser do
defp to_underscore(<<>>, acc),
do: acc
defp get_option_key(option, allow_nonexistent_atoms?) do
if string = to_underscore(option) do
to_existing_key(string, allow_nonexistent_atoms?)
end
end
defp to_existing_key(option, true),
do: String.to_atom(option)
defp to_existing_key(option, false) do
try do
String.to_existing_atom(option)
rescue
ArgumentError -> nil
defp get_option(option) do
if str = to_underscore(option) do
String.to_atom(str)
end
end
@@ -726,35 +618,34 @@ defmodule OptionParser do
match?({_, ""}, Float.parse(arg))
end
defp format_errors([_ | _] = errors, opts) do
defp format_errors(errors, opts) do
types = opts[:switches] || opts[:strict]
error_count = length(errors)
error = if error_count == 1, do: "error", else: "errors"
"#{error_count} #{error} found!\n" <>
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
info = Enum.map(errors, &format_error(&1, opts, types))
total = length(errors)
error = if total == 1, do: "error", else: "errors"
"#{total} #{error} found!#{info}"
end
defp format_error({option, nil}, opts, types) do
if type = get_type(option, opts, types) do
"#{option} : Missing argument of type #{type}"
"\n#{option} : Missing argument of type #{type}"
else
"#{option} : Unknown option"
"\n#{option} : Unknown option"
end
end
defp format_error({option, value}, opts, types) do
type = get_type(option, opts, types)
"#{option} : Expected type #{type}, got #{inspect value}"
"\n#{option} : Expected type #{type}, got #{inspect value}"
end
defp get_type(option, opts, types) do
allow_nonexistent_atoms? = opts[:allow_nonexistent_atoms] || false
key = option |> String.trim_leading("-") |> get_option_key(allow_nonexistent_atoms?)
option_key = option |> String.trim_leading("-") |> get_option()
if option_key = opts[:aliases][key] do
types[option_key]
if option_alias = opts[:aliases][option_key] do
types[option_alias]
else
types[key]
types[option_key]
end
end
end
+95 -119
View File
@@ -12,15 +12,14 @@ defmodule Path do
that require it (like `wildcard/2` and `expand/1`).
"""
alias :filename, as: FN
@type t :: :unicode.chardata()
@doc """
Converts the given path to an absolute one. Unlike
`expand/1`, no attempt is made to resolve `..`, `.` or `~`.
## Examples
### Unix
## Unix examples
Path.absname("foo")
#=> "/usr/local/foo"
@@ -28,12 +27,12 @@ defmodule Path do
Path.absname("../x")
#=> "/usr/local/../x"
### Windows
## Windows
Path.absname("foo").
#=> "D:/usr/local/foo"
"D:/usr/local/foo"
Path.absname("../x").
#=> "D:/usr/local/../x"
"D:/usr/local/../x"
"""
@spec absname(t) :: binary
@@ -42,10 +41,8 @@ defmodule Path do
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`.
Builds a path from `relative_to` to `path`. If `path` is already
an absolute path, `relative_to` is ignored. See also `relative_to/2`.
Unlike `expand/2`, no attempt is made to
resolve `..`, `.` or `~`.
@@ -100,7 +97,7 @@ defmodule Path do
do: do_absname_join(IO.chardata_to_string(left), relative(right), [], major_os_type())
defp do_absname_join(<<uc_letter, ?:, rest::binary>>, relativename, [], :win32) when uc_letter in ?A..?Z, do:
do_absname_join(rest, relativename, [?:, uc_letter + ?a - ?A], :win32)
do_absname_join(rest, relativename, [?:, uc_letter+?a-?A], :win32)
defp do_absname_join(<<?\\, rest::binary>>, relativename, result, :win32), do:
do_absname_join(<<?/, rest::binary>>, relativename, result, :win32)
defp do_absname_join(<<?/, rest::binary>>, relativename, [?., ?/ | result], os_type), do:
@@ -108,7 +105,7 @@ defmodule Path do
defp do_absname_join(<<?/, rest::binary>>, relativename, [?/ | result], os_type), do:
do_absname_join(rest, relativename, [?/ | result], os_type)
defp do_absname_join(<<>>, <<>>, result, os_type), do:
IO.iodata_to_binary(reverse_maybe_remove_dir_sep(result, os_type))
IO.iodata_to_binary(reverse_maybe_remove_dirsep(result, os_type))
defp do_absname_join(<<>>, relativename, [?: | rest], :win32), do:
do_absname_join(relativename, <<>>, [?: | rest], :win32)
defp do_absname_join(<<>>, relativename, [?/ | result], os_type), do:
@@ -118,13 +115,13 @@ defmodule Path do
defp do_absname_join(<<char, rest::binary>>, relativename, result, os_type), do:
do_absname_join(rest, relativename, [char | result], os_type)
defp reverse_maybe_remove_dir_sep([?/, ?:, letter], :win32), do:
defp reverse_maybe_remove_dirsep([?/, ?:, letter], :win32), do:
[letter, ?:, ?/]
defp reverse_maybe_remove_dir_sep([?/], _), do:
defp reverse_maybe_remove_dirsep([?/], _), do:
[?/]
defp reverse_maybe_remove_dir_sep([?/ | name], _), do:
defp reverse_maybe_remove_dirsep([?/ | name], _), do:
:lists.reverse(name)
defp reverse_maybe_remove_dir_sep(name, _), do:
defp reverse_maybe_remove_dirsep(name, _), do:
:lists.reverse(name)
@doc """
@@ -144,11 +141,10 @@ defmodule Path do
@doc """
Expands the path relative to the path given as the second argument
expanding any `.` and `..` characters.
expanding any `.` and `..` characters. If the path is already an
absolute path, `relative_to` is ignored.
If the path is already an absolute path, `relative_to` is ignored.
Note that this function treats a `path` with a leading `~` as
Note, that this function treats `path` with a leading `~` as
an absolute one.
The second argument is first expanded to an absolute path.
@@ -173,9 +169,7 @@ defmodule Path do
@doc """
Returns the path type.
## Examples
### Unix
## Unix examples
Path.type("/") #=> :absolute
Path.type("/usr/local/bin") #=> :absolute
@@ -183,7 +177,7 @@ defmodule Path do
Path.type("../usr/local/bin") #=> :relative
Path.type("~/file") #=> :relative
### Windows
## Windows examples
Path.type("D:/usr/local/bin") #=> :absolute
Path.type("usr/local/bin") #=> :relative
@@ -192,24 +186,20 @@ defmodule Path do
"""
@spec type(t) :: :absolute | :relative | :volumerelative
def type(name)
when is_list(name)
when is_binary(name) do
pathtype(name, major_os_type()) |> elem(0)
def type(name) when is_list(name) or is_binary(name) do
pathtype(name, major_os_type) |> elem(0)
end
@doc """
Forces the path to be a relative path.
## Examples
### Unix
## Unix examples
Path.relative("/usr/local/bin") #=> "usr/local/bin"
Path.relative("usr/local/bin") #=> "usr/local/bin"
Path.relative("../usr/local/bin") #=> "../usr/local/bin"
### Windows
## Windows examples
Path.relative("D:/usr/local/bin") #=> "usr/local/bin"
Path.relative("usr/local/bin") #=> "usr/local/bin"
@@ -235,49 +225,46 @@ defmodule Path do
end
end
defp unix_pathtype(path) when path in ["/", '/'],
do: {:absolute, "."}
defp unix_pathtype(<<?/, relative::binary>>),
do: {:absolute, relative}
defp unix_pathtype([?/ | relative]),
do: {:absolute, relative}
defp unix_pathtype([list | rest]) when is_list(list),
do: unix_pathtype(list ++ rest)
defp unix_pathtype(relative),
do: {:relative, relative}
defp unix_pathtype(<<?/, relative::binary>>), do:
{:absolute, relative}
defp unix_pathtype([?/ | relative]), do:
{:absolute, relative}
defp unix_pathtype([list | rest]) when is_list(list), do:
unix_pathtype(list ++ rest)
defp unix_pathtype(relative), do:
{:relative, relative}
@slash [?/, ?\\]
defp win32_pathtype([list | rest]) when is_list(list),
do: win32_pathtype(list ++ rest)
defp win32_pathtype([char, list | rest]) when is_list(list),
do: win32_pathtype([char | list ++ rest])
defp win32_pathtype(<<c1, c2, relative::binary>>) when c1 in @slash and c2 in @slash,
do: {:absolute, relative}
defp win32_pathtype(<<char, relative::binary>>) when char in @slash,
do: {:volumerelative, relative}
defp win32_pathtype(<<_letter, ?:, char, relative::binary>>) when char in @slash,
do: {:absolute, relative}
defp win32_pathtype(<<_letter, ?:, relative::binary>>),
do: {:volumerelative, relative}
defp win32_pathtype([list | rest]) when is_list(list), do:
win32_pathtype(list++rest)
defp win32_pathtype([char, list | rest]) when is_list(list), do:
win32_pathtype([char | list++rest])
defp win32_pathtype(<<c1, c2, relative::binary>>) when c1 in @slash and c2 in @slash, do:
{:absolute, relative}
defp win32_pathtype(<<c, relative::binary>>) when c in @slash, do:
{:volumerelative, relative}
defp win32_pathtype(<<_letter, ?:, c, relative::binary>>) when c in @slash, do:
{:absolute, relative}
defp win32_pathtype(<<_letter, ?:, relative::binary>>), do:
{:volumerelative, relative}
defp win32_pathtype([c1, c2 | relative]) when c1 in @slash and c2 in @slash,
do: {:absolute, relative}
defp win32_pathtype([char | relative]) when char in @slash,
do: {:volumerelative, relative}
defp win32_pathtype([c1, c2, list | rest]) when is_list(list),
do: win32_pathtype([c1, c2 | list ++ rest])
defp win32_pathtype([_letter, ?:, char | relative]) when char in @slash,
do: {:absolute, relative}
defp win32_pathtype([_letter, ?: | relative]),
do: {:volumerelative, relative}
defp win32_pathtype(relative),
do: {:relative, relative}
defp win32_pathtype([c1, c2 | relative]) when c1 in @slash and c2 in @slash, do:
{:absolute, relative}
defp win32_pathtype([c | relative]) when c in @slash, do:
{:volumerelative, relative}
defp win32_pathtype([c1, c2, list | rest]) when is_list(list), do:
win32_pathtype([c1, c2 | list++rest])
defp win32_pathtype([_letter, ?:, c | relative]) when c in @slash, do:
{:absolute, relative}
defp win32_pathtype([_letter, ?: | relative]), do:
{:volumerelative, relative}
defp win32_pathtype(relative), do:
{:relative, relative}
@doc """
Returns the given `path` relative to the given `from` path.
In other words, this function tries to strip the `from` prefix from `path`.
In other words, it tries to strip the `from` prefix from `path`.
This function does not query the file system, so it assumes
no symlinks between the paths.
@@ -317,10 +304,8 @@ defmodule Path do
@doc """
Convenience to get the path relative to the current working
directory.
If, for some reason, the current working directory
cannot be retrieved, this function returns the given `path`.
directory. If, for some reason, the current working directory
cannot be retrieved, returns the full path.
"""
@spec relative_to_cwd(t) :: binary
def relative_to_cwd(path) do
@@ -348,15 +333,13 @@ defmodule Path do
"""
@spec basename(t) :: binary
def basename(path) do
:filename.basename(IO.chardata_to_string(path))
FN.basename(IO.chardata_to_string(path))
end
@doc """
Returns the last component of `path` with the `extension`
stripped.
This function should be used to remove a specific
extension which may or may not be there.
stripped. This function should be used to remove a specific
extension which may, or may not, be there.
## Examples
@@ -372,7 +355,7 @@ defmodule Path do
"""
@spec basename(t, t) :: binary
def basename(path, extension) do
:filename.basename(IO.chardata_to_string(path), IO.chardata_to_string(extension))
FN.basename(IO.chardata_to_string(path), IO.chardata_to_string(extension))
end
@doc """
@@ -386,13 +369,10 @@ defmodule Path do
iex> Path.dirname("/foo/bar/baz.ex")
"/foo/bar"
iex> Path.dirname("/foo/bar/")
"/foo/bar"
"""
@spec dirname(t) :: binary
def dirname(path) do
:filename.dirname(IO.chardata_to_string(path))
FN.dirname(IO.chardata_to_string(path))
end
@doc """
@@ -409,7 +389,7 @@ defmodule Path do
"""
@spec extname(t) :: binary
def extname(path) do
:filename.extension(IO.chardata_to_string(path))
FN.extension(IO.chardata_to_string(path))
end
@doc """
@@ -426,14 +406,12 @@ defmodule Path do
"""
@spec rootname(t) :: binary
def rootname(path) do
:filename.rootname(IO.chardata_to_string(path))
FN.rootname(IO.chardata_to_string(path))
end
@doc """
Returns the `path` with the `extension` stripped.
This function should be used to remove a specific extension which may
or may not be there.
Returns the `path` with the `extension` stripped. This function should be used to
remove a specific extension which might, or might not, be there.
## Examples
@@ -446,14 +424,14 @@ defmodule Path do
"""
@spec rootname(t, t) :: binary
def rootname(path, extension) do
:filename.rootname(IO.chardata_to_string(path), IO.chardata_to_string(extension))
FN.rootname(IO.chardata_to_string(path), IO.chardata_to_string(extension))
end
@doc """
Joins a list of paths.
Joins a list of strings.
This function should be used to convert a list of paths to a path.
Note that any trailing slash is removed when joining.
This function should be used to convert a list of strings to a path.
Note that any trailing slash is removed on join.
## Examples
@@ -467,41 +445,38 @@ defmodule Path do
"/foo/bar"
"""
@spec join(nonempty_list(t)) :: binary
@spec join([t]) :: binary
def join([name1, name2 | rest]), do:
join([join(name1, name2) | rest])
def join([name]), do:
IO.chardata_to_string(name)
name
@doc """
Joins two paths.
The right path will always be expanded to its relative format
and any trailing slash will be removed when joining.
and any trailing slash is removed on join.
## Examples
iex> Path.join("foo", "bar")
"foo/bar"
iex> Path.join("/foo", "/bar/")
"/foo/bar"
"""
@spec join(t, t) :: binary
def join(left, right) do
left = IO.chardata_to_string(left)
os_type = major_os_type()
do_join(left, right, os_type) |> remove_dir_sep(os_type)
do_join(left, right, os_type) |> remove_dirsep(os_type)
end
defp do_join("", right, os_type), do: relative(right, os_type)
defp do_join("/", right, os_type), do: "/" <> relative(right, os_type)
defp do_join(left, right, os_type), do: remove_dir_sep(left, os_type) <> "/" <> relative(right, os_type)
defp do_join(left, right, os_type), do: remove_dirsep(left, os_type) <> "/" <> relative(right, os_type)
defp remove_dir_sep("", _os_type), do: ""
defp remove_dir_sep("/", _os_type), do: "/"
defp remove_dir_sep(bin, os_type) do
defp remove_dirsep("", _os_type), do: ""
defp remove_dirsep("/", _os_type), do: "/"
defp remove_dirsep(bin, os_type) do
last = :binary.last(bin)
if last == ?/ or (last == ?\\ and os_type == :win32) do
binary_part(bin, 0, byte_size(bin) - 1)
@@ -537,7 +512,7 @@ defmodule Path do
def split(""), do: []
def split(path) do
:filename.split(IO.chardata_to_string(path))
FN.split(IO.chardata_to_string(path))
end
defmodule Wildcard do
@@ -547,8 +522,13 @@ defmodule Path do
call({:read_link_info, file})
end
# For compatibility with buggy Erlang 17.1.
def read_file_info(file) do
call({:read_link_info, file})
end
def list_dir(dir) do
case call({:list_dir, dir}) do
case call({:list_dir, dir}) do
{:ok, files} ->
{:ok, for(file <- files, hd(file) != ?., do: file)}
other ->
@@ -567,7 +547,7 @@ defmodule Path do
end
@doc """
Traverses paths according to the given `glob` expression and returns a
Traverses paths according to the given `glob` expression, and returns a
list of matches.
The wildcard looks like an ordinary path, except that certain
@@ -593,7 +573,7 @@ defmodule Path do
Other characters represent themselves. Only paths that have
exactly the same character in the same position will match. Note
that matching is case-sensitive: `"a"` will not match `"A"`.
that matching is case-sensitive; i.e. "a" will not match "A".
By default, the patterns `*` and `?` do not match files starting
with a dot `.` unless `match_dot: true` is given in `opts`.
@@ -601,7 +581,7 @@ defmodule Path do
## Examples
Imagine you have a directory called `projects` with three Elixir projects
inside of it: `elixir`, `ex_doc`, and `plug`. You can find all `.beam` files
inside of it: `elixir`, `ex_doc` and `dynamo`. You can find all `.beam` files
inside the `ebin` directory of each project as follows:
Path.wildcard("projects/*/ebin/**/*.beam")
@@ -611,23 +591,21 @@ defmodule Path do
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
"""
@spec wildcard(t, keyword) :: [binary]
@spec wildcard(t, Keyword.t) :: [binary]
def wildcard(glob, opts \\ []) do
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
glob
|> chardata_to_list!()
|> chardata_to_list()
|> :filelib.wildcard(mod)
|> Enum.map(&IO.chardata_to_string/1)
end
defp chardata_to_list!(chardata) do
# expand_dot the given path by expanding "..", "." and "~".
defp chardata_to_list(chardata) do
case :unicode.characters_to_list(chardata) do
result when is_list(result) ->
if 0 in result do
raise ArgumentError, "cannot execute Path.wildcard/2 for path with null byte, got: #{inspect chardata}"
else
result
end
result
{:error, encoded, rest} ->
raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid
@@ -647,7 +625,7 @@ defmodule Path do
defp resolve_home(""), do: System.user_home!
defp resolve_home(rest) do
case {rest, major_os_type()} do
case {rest, major_os_type} do
{"\\" <> _, :win32} ->
System.user_home! <> rest
{"/" <> _, _} ->
@@ -656,8 +634,6 @@ defmodule Path do
end
end
# expand_dot the given path by expanding "..", "." and "~".
defp expand_dot(<<"/", rest::binary>>),
do: "/" <> do_expand_dot(rest)
defp expand_dot(<<letter, ":/", rest::binary>>) when letter in ?a..?z,
+52 -175
View File
@@ -1,163 +1,6 @@
defmodule Port do
@moduledoc ~S"""
Functions for interacting with the external world through ports.
Ports provide a mechanism to start operating system processes external
to the Erlang VM and communicate with them via message passing.
## Example
iex> port = Port.open({:spawn, "cat"}, [:binary])
iex> send port, {self(), {:command, "hello"}}
iex> send port, {self(), {:command, "world"}}
iex> flush()
{#Port<0.1444>, {:data, "hello"}}
{#Port<0.1444>, {:data, "world"}}
iex> send port, {self(), :close}
:ok
iex> flush()
{#Port<0.1464>, :closed}
:ok
In the example above, we have created a new port that executes the
program `cat`. `cat` is a program available on UNIX systems that
receives data from multiple inputs and concatenates them in the output.
After the port was created, we sent it two commands in the form of
messages using `Kernel.send/2`. The first command has the binary payload
of "hello" and the second has "world".
After sending those two messages, we invoked the IEx helper `flush()`,
which printed all messages received from the port, in this case we got
"hello" and "world" back. Notice the messages are in binary because we
passed the `:binary` option when opening the port in `Port.open/2`. Without
such option, it would have yielded a list of bytes.
Once everything was done, we closed the port.
Elixir provides many conveniences for working with ports and some drawbacks.
We will explore those below.
## Message and function APIs
There are two APIs for working with ports. It can be either asynchronous via
message passing, as in the example above, or by calling the functions on this
module.
The messages supported by ports and their counterpart function APIs are
listed below:
* `{pid, {:command, binary}}` - sends the given data to the port.
See `command/3`.
* `{pid, :close}` - closes the port. Unless the port is already closed,
the port will reply with `{port, :closed}` message once it has flushed
its buffers and effectively closed. See `close/1`.
* `{pid, {:connect, new_pid}}` - sets the `new_pid` as the new owner of
the port. Once a port is opened, the port is linked and connected to the
caller process and communication to the port only happens through the
connected process. This message makes `new_pid` the new connected processes.
Unless the port is dead, the port will reply to the old owner with
`{port, :connected}`. See `connect/2`.
On its turn, the port will send the connected process the following messages:
* `{port, {:data, data}}` - data sent by the port
* `{port, :closed}` - reply to the `{pid, :close}` message
* `{port, :connected}` - reply to the `{pid, {:connect, new_pid}}` message
* `{:EXIT, port, reason}` - exit signals in case the port crashes. If reason
is not `:normal`, this message will only be received if the owner process
is trapping exits
## Open mechanisms
The port can be opened through four main mechanisms.
As a short summary, prefer to using the `:spawn` and `:spawn_executable`
options mentioned below. The other two options, `:spawn_driver` and `:fd`
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.
### spawn
The `:spawn` tuple receives a binary that is going to be executed as a
full invocation. For example, we can use it to invoke "echo hello" directly:
iex> port = Port.open({:spawn, "echo oops"}, [:binary])
iex> flush()
{#Port<0.1444>, {:data, "oops\n"}}
`:spawn` will retrieve the program name from the argument and traverse your
OS `$PATH` environment variable looking for a matching program.
Although the above is handy, it means it is impossible to invoke an executable
that has whitespaces on its name or in any of its arguments. For those reasons,
most times it is preferrable to execute `:spawn_executable`.
### spawn_executable
Spawn executable is a more restricted and explicit version of spawn. It expects
full file paths to the executable you want to execute. If they are in your `$PATH`,
they can be retrieved by calling `System.find_executable/1`:
iex> path = System.find_executable("echo")
iex> port = Port.open({:spawn_executable, path}, [:binary, args: ["hello world"]])
iex> flush()
{#Port<0.1380>, {:data, "hello world\n"}}
When using `:spawn_executable`, the list of arguments can be passed via
the `:args` option as done above. For the full list of options, see the
documentation for the Erlang function `:erlang.open_port/2`.
### spawn_driver
Spawn driver is used to start Port Drivers, which are programs written in
C that implements a specific communication protocols and are dynamically
linked to the Erlang VM. Port drivers are an advanced topic and one of the
mechanisms for integrating C code, alongside NIFs. For more information,
[please check the Erlang docs](http://erlang.org/doc/reference_manual/ports.html).
### fd
The `:fd` name option allows developers to access `in` and `out` file
descriptors used by the Erlang VM. You would use those only if you are
reimplementing core part of the Runtime System, such as the `:user` and
`:shell` processes.
## Zombie processes
A port can be closed via the `close/1` function or by sending a `{pid, :close}`
message. However, if the VM crashes, a long-running program started by the port
will have its stdin and stdout channels closed but **it won't be automatically
terminated**.
While most UNIX command line tools will exit once its communication channels
are closed, not all command line applications will do so. While we encourage
graceful termination by detecting if stdin/stdout has been closed, we do not
always have control over how 3rd party software terminates. In those cases,
you can wrap the application in a script that checks for stdin. Here is such
script in bash:
#!/bin/sh
"$@"
pid=$!
while read line ; do
:
done
kill -KILL $pid
Now instead of:
Port.open({:spawn_executable, "/path/to/program"},
[args: ["a", "b", "c"]])
You may invoke:
Port.open({:spawn_executable, "/path/to/wrapper"},
[args: ["/path/to/program", "a", "b", "c"]])
@moduledoc """
Functions related to Erlang ports.
"""
@type name :: {:spawn, charlist | binary} |
@@ -166,22 +9,24 @@ defmodule Port do
{:fd, non_neg_integer, non_neg_integer}
@doc """
Opens a port given a tuple `name` and a list of `options`.
Opens an Erlang port given a tuple `name` and a list of `settings`.
The module documentation above contains documentation and examples
for the supported `name` values, summarized below:
## Name
* `{:spawn, command}` - runs an external program. `command` must contain
the program name and optionally a list of arguments separated by space.
If passing programs or arguments with space in their name, use the next option.
* `{:spawn_executable, filename}` - runs the executable given by the absolute
file name `filename`. Arguments can be passed via the `:args` option.
* `{:spawn_driver, command}` - spawns so-called port drivers.
* `{:fd, fd_in, fd_out}` - accesses file descriptors, `fd_in` and `fd_out`
opened by the VM.
The supported values for `name` are:
For more information and the list of options, see
[`:erlang.open_port/2`](http://www.erlang.org/doc/man/erlang.html#open_port-2).
* `{:spawn, command}` - to run an external program. The first space separated
word of `command` will be considered as the name of the program to run, so
use `{:spawn_executable, command}` to run a program having spaces in its name.
* `{:spawn_driver, command}` - similar to `{:spawn, command}`, but to run a
loaded driver.
* `{:spawn_executable, filename}` - similar to `{:spawn, filename}`, but to run
an external executable. With this option, `filename` in its whole is considered
the name of the program to execute.
* `{:fd, fd_in, fd_out}` - to access file descriptors used by Erlang, `fd_in`
being used for standard input, `fd_out` for standard output.
For more information, see [`:erlang.open_port/2`](http://www.erlang.org/doc/man/erlang.html#open_port-2).
Inlined by the compiler.
"""
@@ -227,7 +72,36 @@ defmodule Port do
end
@doc """
Returns information about the `port` or `nil` if the port is closed.
Sends a synchronous control command to the `port` and returns its reply as a binary.
Not all port drivers support this feature.
For more information, see [`:erlang.port_control/3`](http://www.erlang.org/doc/man/erlang.html#port_control-3).
Inlined by the compiler.
"""
@spec control(port, integer, iodata) :: iodata | binary
def control(port, operation, data) do
:erlang.port_control(port, operation, data)
end
@doc """
Makes a synchronous call to the `port` and returns its reply as a term.
Not all port drivers support this control feature.
For more information, see [`:erlang.port_call/3`](http://www.erlang.org/doc/man/erlang.html#port_call-3).
Inlined by the compiler.
"""
@spec call(port, integer, term) :: term
def call(port, operation, data) do
:erlang.port_call(port, operation, data)
end
@doc """
Returns information about the `port`
or `nil` if the port is closed.
For more information, see [`:erlang.port_info/1`](http://www.erlang.org/doc/man/erlang.html#port_info-1).
"""
@@ -236,7 +110,8 @@ defmodule Port do
end
@doc """
Returns information about the `port` or `nil` if the port is closed.
Returns information about the `port`
or `nil` if the port is closed.
For more information, see [`:erlang.port_info/2`](http://www.erlang.org/doc/man/erlang.html#port_info-2).
"""
@@ -255,7 +130,9 @@ defmodule Port do
end
@doc """
Returns a list of all ports in the current node.
Returns a list of the ports for the current node.
For more information, see [`:erlang.ports/0`](http://www.erlang.org/doc/man/erlang.html#ports-0).
Inlined by the compiler.
"""
+170 -289
View File
@@ -4,7 +4,7 @@ defmodule Process do
Besides the functions available in this module, the `Kernel` module
exposes and auto-imports some basic functionality related to processes
available through the following functions:
available through the functions:
* `Kernel.spawn/1` and `Kernel.spawn/3`
* `Kernel.spawn_link/1` and `Kernel.spawn_link/3`
@@ -15,31 +15,32 @@ defmodule Process do
"""
@doc """
Tells whether the given process is alive.
Returns `true` if the process exists and is alive (i.e. it is not exiting
and has not exited yet). Otherwise, returns `false`.
If the process identified by `pid` is alive (that is, it's not exiting and has
not exited yet) than this function returns `true`. Otherwise, it returns
`false`.
`pid` must refer to a process running on the local node.
`pid` must refer to a process at the local node.
Inlined by the compiler.
"""
@spec alive?(pid) :: boolean
defdelegate alive?(pid), to: :erlang, as: :is_process_alive
def alive?(pid) do
:erlang.is_process_alive(pid)
end
@doc """
Returns all key-value pairs in the process dictionary.
Inlined by the compiler.
"""
@spec get() :: [{term, term}]
defdelegate get(), to: :erlang
@spec get :: [{term, term}]
def get do
:erlang.get()
end
@doc """
Returns the value for the given `key` in the process dictionary,
or `default` if `key` is not set.
Returns the value for the given `key` or `default` if `key` is not set.
"""
@spec get(term) :: term
@spec get(term, default :: term) :: term
def get(key, default \\ nil) do
case :erlang.get(key) do
@@ -56,30 +57,25 @@ defmodule Process do
Inlined by the compiler.
"""
@spec get_keys() :: [term]
defdelegate get_keys(), to: :erlang
def get_keys() do
:erlang.get_keys()
end
@doc """
Returns all keys in the process dictionary that have the given `value`.
Returns all keys that have the given `value`.
Inlined by the compiler.
"""
@spec get_keys(term) :: [term]
defdelegate get_keys(value), to: :erlang
def get_keys(value) do
:erlang.get_keys(value)
end
@doc """
Stores the given `key`-`value` pair in the process dictionary.
The return value of this function is the value that was previously stored
under `key`, or `nil` in case no value was stored under `key`.
## Examples
# Assuming :locale was not set
Process.put(:locale, "en")
#=> nil
Process.put(:locale, "fr")
#=> "en"
The return value is the value that was previously stored under the key `key`
(or `nil` in case no value was stored under `key`).
"""
@spec put(term, term) :: term | nil
def put(key, value) do
@@ -88,18 +84,6 @@ defmodule Process do
@doc """
Deletes the given `key` from the process dictionary.
Returns the value that was under `key` in the process dictionary,
or `nil` if `key` was not stored in the process dictionary.
## Examples
Process.put(:comments, ["comment", "other comment"])
Process.delete(:comments)
#=> ["comment", "other comment"]
Process.delete(:comments)
#=> nil
"""
@spec delete(term) :: term | nil
def delete(key) do
@@ -107,7 +91,7 @@ defmodule Process do
end
@doc """
Sends an exit signal with the given `reason` to `pid`.
Sends an exit signal with the given `reason` to the `pid`.
The following behaviour applies if `reason` is any term except `:normal`
or `:kill`:
@@ -119,42 +103,42 @@ defmodule Process do
message `{:EXIT, from, reason}` and delivered to the message queue
of `pid`.
If `reason` is the atom `:normal`, `pid` will not exit (unless `pid` is
the calling process, in which case it will exit with the reason `:normal`).
If it is trapping exits, the exit signal is transformed into a message
`{:EXIT, from, :normal}` and delivered to its message queue.
3. If `reason` is the atom `:normal`, `pid` will not exit (unless it
is the calling process's pid, in which case it will exit with the
reason `:normal`). If it is trapping exits, the exit signal is
transformed into a message `{:EXIT, from, :normal}` and delivered
to its message queue.
If `reason` is the atom `:kill`, that is if `Process.exit(pid, :kill)` is called,
an untrappable exit signal is sent to `pid` which will unconditionally exit
with reason `:killed`.
4. If `reason` is the atom `:kill`, that is if `exit(pid, :kill)` is
called, an untrappable exit signal is sent to `pid` which will
unconditionally exit with exit reason `:killed`.
Inlined by the compiler.
## Examples
Process.exit(pid, :kill)
#=> true
"""
@spec exit(pid, term) :: true
defdelegate exit(pid, reason), to: :erlang
def exit(pid, reason) do
:erlang.exit(pid, reason)
end
@doc """
Sleeps the current process for the given `timeout`.
Sleeps the current process by `timeout`.
`timeout` is either the number of milliseconds to sleep as an
integer or the atom `:infinity`. When `:infinity` is given,
the current process will sleep forever, and not
consume or reply to messages.
the current process will suspend forever.
**Use this function with extreme care**. For almost all situations
where you would use `sleep/1` in Elixir, there is likely a
more correct, faster and precise way of achieving the same with
more correct, faster and precise way of achieving it with
message passing.
For example, if you are waiting for a process to perform some
action, it is better to communicate the progress of such action
with messages.
For example, if you are waiting a process to perform some
action, it is better to communicate.
In other words, **do not**:
@@ -181,11 +165,11 @@ defmodule Process do
30_000 -> :timeout # Optional timeout
end
For cases like the one above, `Task.async/1` and `Task.await/2` are
preferred.
Or even use `Task.async/1` and `Task.await/2` in the example
above.
Similarly, if you are waiting for a process to terminate,
monitor that process instead of sleeping. **Do not**:
use monitor instead of sleep. **Do not**:
Task.start_link fn ->
...
@@ -209,7 +193,6 @@ defmodule Process do
end
"""
@spec sleep(timeout) :: :ok
def sleep(timeout)
when is_integer(timeout) and timeout >= 0
when timeout == :infinity do
@@ -219,102 +202,69 @@ defmodule Process do
@doc """
Sends a message to the given process.
## Options
If the option `:noconnect` is used and sending the message would require an
auto-connection to another node the message is not sent and `:noconnect` is
returned.
* `:noconnect` - when used, if sending the message would require an
auto-connection to another node the message is not sent and `:noconnect` is
returned.
* `:nosuspend` - when used, if sending the message would cause the sender to
be suspended the message is not sent and `:nosuspend` is returned.
If the option `:nosuspend` is used and sending the message would cause the
sender to be suspended the message is not sent and `:nosuspend` is returned.
Otherwise the message is sent and `:ok` is returned.
## Examples
iex> Process.send({:name, :node_that_does_not_exist}, :hi, [:noconnect])
iex> Process.send({:name, :node_does_not_exist}, :hi, [:noconnect])
:noconnect
Inlined by the compiler.
"""
@spec send(dest, msg, [option]) :: :ok | :noconnect | :nosuspend
when dest: pid | port | atom | {atom, node},
msg: any,
option: :noconnect | :nosuspend
defdelegate send(dest, msg, options), to: :erlang
@spec send(dest, msg, [option]) :: :ok | :noconnect | :nosuspend when
dest: pid | port | atom | {atom, node},
msg: any,
option: :noconnect | :nosuspend
def send(dest, msg, options) do
:erlang.send(dest, msg, options)
end
@doc """
Sends `msg` to `dest` after `time` milliseconds.
If `dest` is a PID, it must be the PID of a local process, dead or alive.
If `dest` is a pid, it must be the pid of a local process, dead or alive.
If `dest` is an atom, it must be the name of a registered process
which is looked up at the time of delivery. No error is produced if the name does
which is looked up at the time of delivery. No error is given if the name does
not refer to a process.
This function returns a timer reference, which can be read with `read_timer/1`
or canceled with `cancel_timer/1`.
This function returns a timer reference, which can be read or canceled with
`read_timer/1` and `cancel_timer/1`.
The timer will be automatically canceled if the given `dest` is a PID
which is not alive or when the given PID exits. Note that timers will not be
Finally, the timer will be automatically canceled if the given `dest` is a pid
which is not alive or when the given pid exits. Note that timers will not be
automatically canceled when `dest` is an atom (as the atom resolution is done
on delivery).
Inlined by the compiler.
## Options
* `:abs` - (boolean) when `false`, `time` is treated as relative to the
current monotonic time. When `true`, `time` is the absolute value of the
Erlang monotonic time at which `msg` should be delivered to `dest`.
To read more about Erlang monotonic time and other time-related concepts,
look at the documentation for the `System` module. Defaults to `false`.
## Examples
timer_ref = Process.send_after(pid, :hi, 1000)
"""
@spec send_after(pid | atom, term, non_neg_integer, [option]) :: reference
when option: {:abs, boolean}
def send_after(dest, msg, time, opts \\ []) do
:erlang.send_after(time, dest, msg, opts)
@spec send_after(pid | atom, term, non_neg_integer) :: reference
def send_after(dest, msg, time) do
:erlang.send_after(time, dest, msg)
end
@doc """
Cancels a timer returned by `send_after/3`.
Cancels a timer created by `send_after/3`.
When the result is an integer, it represents the time in milliseconds
left until the timer would have expired.
When the result is `false`, a timer corresponding to `timer_ref` could not be
found. This can happen either because the timer expired, because it has
already been canceled, or because `timer_ref` never corresponded to a timer.
When the result is `false`, a timer corresponding to `timer_ref` could
not be found. This can be either because the timer expired, already has
been canceled, or because `timer_ref` never corresponded to a timer.
Even if the timer had expired and the message was sent, this function does not
tell you if the timeout message has arrived at its destination yet.
## Options
* `:async` - (boolean) when `false`, the request for cancellation is
synchronous. When `true`, the request for cancellation is asynchronous,
meaning that the request to cancel the timer is issued and `:ok` is
returned right away. Defaults to `false`.
* `:info` - (boolean) whether to return information about the timer being
cancelled. When the `:async` option is `false` and `:info` is `true`, then
either an integer or `false` (like described above) is returned. If
`:async` is `false` and `:info` is `false`, `:ok` is returned. If `:async`
is `true` and `:info` is `true`, a message in the form `{:cancel_timer,
timer_ref, result}` (where `result` is an integer or `false` like
described above) is sent to the caller of this function when the
cancellation has been performed. If `:async` is `true` and `:info` is
`false`, no message is sent. Defaults to `true`.
If the timer has expired, the timeout message has been sent, but it does
not tell you whether or not it has arrived at its destination yet.
Inlined by the compiler.
"""
@spec cancel_timer(reference, options) :: non_neg_integer | false | :ok
when options: [async: boolean, info: boolean]
defdelegate cancel_timer(timer_ref, options \\ []), to: :erlang
@spec cancel_timer(reference) :: non_neg_integer | false
def cancel_timer(timer_ref) do
:erlang.cancel_timer(timer_ref)
end
@doc """
Reads a timer created by `send_after/3`.
@@ -322,17 +272,19 @@ defmodule Process do
When the result is an integer, it represents the time in milliseconds
left until the timer will expire.
When the result is `false`, a timer corresponding to `timer_ref` could not be
found. This can be either because the timer expired, because it has already
When the result is `false`, a timer corresponding to `timer_ref` could
not be found. This can be either because the timer expired, already has
been canceled, or because `timer_ref` never corresponded to a timer.
Even if the timer had expired and the message was sent, this function does not
tell you if the timeout message has arrived at its destination yet.
If the timer has expired, the timeout message has been sent, but it does
not tell you whether or not it has arrived at its destination yet.
Inlined by the compiler.
"""
@spec read_timer(reference) :: non_neg_integer | false
defdelegate read_timer(timer_ref), to: :erlang
def read_timer(timer_ref) do
:erlang.read_timer(timer_ref)
end
@type spawn_opt :: :link | :monitor | {:priority, :low | :normal | :high} |
{:fullsweep_after, non_neg_integer} |
@@ -345,25 +297,27 @@ defmodule Process do
The result depends on the given options. In particular,
if `:monitor` is given as an option, it will return a tuple
containing the PID and the monitoring reference, otherwise
just the spawned process PID.
containing the pid and the monitoring reference, otherwise
just the spawned process pid.
More options are available; for the comprehensive list of available options
It also accepts extra options, for the list of available options
check [`:erlang.spawn_opt/4`](http://www.erlang.org/doc/man/erlang.html#spawn_opt-4).
Inlined by the compiler.
"""
@spec spawn((() -> any), spawn_opts) :: pid | {pid, reference}
defdelegate spawn(fun, opts), to: :erlang, as: :spawn_opt
def spawn(fun, opts) do
:erlang.spawn_opt(fun, opts)
end
@doc """
Spawns the given function `fun` from module `mod`, passing the given `args`
Spawns the given function from module `mod`, passing the given `args`
according to the given options.
The result depends on the given options. In particular,
if `:monitor` is given as an option, it will return a tuple
containing the PID and the monitoring reference, otherwise
just the spawned process PID.
containing the pid and the monitoring reference, otherwise
just the spawned process pid.
It also accepts extra options, for the list of available options
check [`:erlang.spawn_opt/4`](http://www.erlang.org/doc/man/erlang.html#spawn_opt-4).
@@ -371,22 +325,13 @@ defmodule Process do
Inlined by the compiler.
"""
@spec spawn(module, atom, list, spawn_opts) :: pid | {pid, reference}
defdelegate spawn(mod, fun, args, opts), to: :erlang, as: :spawn_opt
def spawn(mod, fun, args, opts) do
:erlang.spawn_opt(mod, fun, args, opts)
end
@doc """
Starts monitoring the given `item` from the calling process.
Once the monitored process dies, a message is delivered to the
monitoring process in the shape of:
{:DOWN, ref, :process, object, reason}
where:
* `ref` is a monitor reference returned by this function;
* `object` is either a `pid` of the monitored process (if monitoring
a PID) or `{name, node}` (if monitoring a remote or local name);
* `reason` is the exit reason.
The calling process starts monitoring the given `item`.
It returns the monitor reference.
See [the need for monitoring](http://elixir-lang.org/getting-started/mix-otp/genserver.html#the-need-for-monitoring)
for an example.
@@ -400,129 +345,90 @@ defmodule Process do
end
@doc """
Demonitors the monitor identifies by the given `reference`.
If `monitor_ref` is a reference which the calling process
obtained by calling `monitor/1`, that monitoring is turned off.
obtained by calling `monitor/1`, this monitoring is turned off.
If the monitoring is already turned off, nothing happens.
See [`:erlang.demonitor/2`](http://www.erlang.org/doc/man/erlang.html#demonitor-2) for more info.
Inlined by the compiler.
"""
@spec demonitor(reference) :: true
@spec demonitor(reference, options :: [:flush | :info]) :: boolean
defdelegate demonitor(monitor_ref, options \\ []), to: :erlang
def demonitor(monitor_ref, options \\ []) do
:erlang.demonitor(monitor_ref, options)
end
@doc """
Returns a list of PIDs corresponding to all the
Returns a list of process identifiers corresponding to all the
processes currently existing on the local node.
Note that if a process is exiting, it is considered to exist but not be
alive. This means that for such process, `alive?/1` will return `false` but
its PID will be part of the list of PIDs returned by this function.
Note that a process that is exiting, exists but is not alive, i.e.,
`alive?/1` will return `false` for a process that is exiting,
but its process identifier will be part of the result returned.
See [`:erlang.processes/0`](http://www.erlang.org/doc/man/erlang.html#processes-0) for more info.
Inlined by the compiler.
"""
@spec list() :: [pid]
defdelegate list(), to: :erlang, as: :processes
@spec list :: [pid]
def list do
:erlang.processes()
end
@doc """
Creates a link between the calling process and the given item (process or
port).
Links are bidirectional. Linked processes can be unlinked by using `unlink/1`.
If such a link exists already, this function does nothing since there can only
be one link between two given processes. If a process tries to create a link
to itself, nothing will happen.
When two processes are linked, each one receives exit signals from the other
(see also `exit/2`). Let's assume `pid1` and `pid2` are linked. If `pid2`
exits with a reason other than `:normal` (which is also the exit reason used
when a process finishes its job) and `pid1` is not trapping exits (see
`flag/2`), then `pid1` will exit with the same reason as `pid2` and in turn
emit an exit signal to all its other linked processes. The behaviour when
`pid1` is trapping exits is described in `exit/2`.
Creates a link between the calling process and another process
(or port) `pid`, if there is not such a link already.
See [`:erlang.link/1`](http://www.erlang.org/doc/man/erlang.html#link-1) for more info.
Inlined by the compiler.
"""
@spec link(pid | port) :: true
defdelegate link(pid_or_port), to: :erlang
def link(pid) do
:erlang.link(pid)
end
@doc """
Removes the link between the calling process and the given item (process or
port).
If there is no such link, this function does nothing. If `pid_or_port` does
not exist, this function does not produce any errors and simply does nothing.
The return value of this function is always `true`.
Removes the link, if there is one, between the calling process and
the process or port referred to by `pid`. Returns `true` and does not
fail, even if there is no link or `id` does not exist
See [`:erlang.unlink/1`](http://www.erlang.org/doc/man/erlang.html#unlink-1) for more info.
Inlined by the compiler.
"""
@spec unlink(pid | port) :: true
defdelegate unlink(pid_or_port), to: :erlang
@doc """
Registers the given `pid_or_port` under the given `name`.
`name` must be an atom and can then be used instead of the
PID/port identifier when sending messages with `Kernel.send/2`.
`register/2` will fail with `ArgumentError` in any of the following cases:
* the PID/Port is not existing locally and alive
* the name is already registered
* the `pid_or_port` is already registered under a different `name`
The following names are reserved and cannot be assigned to
processes nor ports:
* `nil`
* `false`
* `true`
* `:undefined`
"""
@spec register(pid | port, atom) :: true
def register(pid_or_port, name) when is_atom(name) and name not in [nil, false, true, :undefined] do
:erlang.register(name, pid_or_port)
catch
:error, :badarg when node(pid_or_port) != node() ->
message = "could not register the #{pid_or_port pid_or_port} because it belongs to another node"
:erlang.error ArgumentError.exception(message), [pid_or_port, name]
:error, :badarg ->
message = "could not register the #{pid_or_port pid_or_port} with " <>
"name #{inspect name}. Or it is not alive, or the name is already " <>
"taken, or it has already been given another name"
:erlang.error ArgumentError.exception(message), [pid_or_port, name]
def unlink(pid) do
:erlang.unlink(pid)
end
defp pid_or_port(pid) when is_pid(pid), do: "pid #{inspect pid}"
defp pid_or_port(port) when is_port(port), do: "port #{inspect port}"
@doc """
Associates the atom `name` with a `pid` or a port identifier.
`name`, can then be used instead of the `pid` / port identifier with the `Kernel.send/2`
function. `Process.register/2` will fail with `ArgumentError` if the pid supplied
is no longer alive, (check with `alive?/1`) or if the name is already registered
(check with `whereis/1`) or if the `pid` is already registered to a different `name`.
"""
@spec register(pid | port, atom) :: true
def register(pid, name) when not name in [nil, false, true] and is_atom(name) do
:erlang.register(name, pid)
end
@doc """
Removes the registered `name`, associated with a PID
or a port identifier.
Removes the registered `name`, associated with a pid or a port identifier.
Fails with `ArgumentError` if the name is not registered
to any PID or port.
Fails with `ArgumentError` if the name is not registered to any pid or port.
Inlined by the compiler.
See [`:erlang.unregister/1`](http://www.erlang.org/doc/man/erlang.html#unregister-1) for more info.
"""
@spec unregister(atom) :: true
defdelegate unregister(name), to: :erlang
def unregister(name) do
:erlang.unregister(name)
end
@doc """
Returns the PID or port identifier registered under `name` or `nil` if the
name is not registered.
Returns the pid or port identifier with the registered `name`.
Returns `nil` if the name is not registered.
See [`:erlang.whereis/1`](http://www.erlang.org/doc/man/erlang.html#whereis-1) for more info.
"""
@@ -532,20 +438,16 @@ defmodule Process do
end
@doc """
Returns the PID of the group leader for the calling process.
Inlined by the compiler.
Returns the pid of the group leader for the process which evaluates the function.
"""
@spec group_leader() :: pid
defdelegate group_leader(), to: :erlang
@spec group_leader :: pid
def group_leader do
:erlang.group_leader
end
@doc """
Sets the group leader of the given `pid` to `leader`.
Typically, this is used when a process started from a certain shell should
have a group leader other than `:init`.
Inlined by the compiler.
Sets the group leader of `pid` to `leader`. Typically, this is used when a processes
started from a certain shell should have a group leader other than `:init`.
"""
@spec group_leader(pid, leader :: pid) :: true
def group_leader(pid, leader) do
@@ -554,67 +456,46 @@ defmodule Process do
@doc """
Returns a list of names which have been registered using `register/2`.
Inlined by the compiler.
"""
@spec registered() :: [atom]
defdelegate registered(), to: :erlang
@typep heap_size :: non_neg_integer |
%{size: non_neg_integer, kill: boolean, error_logger: boolean}
@typep priority_level :: :low | :normal | :high | :max
@spec registered :: [atom]
def registered do
:erlang.registered()
end
@typep process_flag :: :trap_exit | :error_handler | :min_heap_size |
:min_bin_vheap_size | :priority | :save_calls |
:sensitive
@doc """
Sets the given `flag` to `value` for the calling process.
Returns the old value of `flag`.
Sets certain flags for the process which calls this function.
Returns the old value of the `flag`.
See [`:erlang.process_flag/2`](http://www.erlang.org/doc/man/erlang.html#process_flag-2) for more info.
Note that `flag` values `:max_heap_size` and `:message_queue_data` are only available since OTP 19.
Inlined by the compiler.
"""
@spec flag(:error_handler, module) :: module
@spec flag(:max_heap_size, heap_size) :: heap_size
@spec flag(:message_queue_data, :erlang.message_queue_data) :: :erlang.message_queue_data
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
@spec flag(:monitor_nodes, term) :: term
@spec flag({:monitor_nodes, term()}, term) :: term
@spec flag(:priority, priority_level) :: priority_level
@spec flag(:save_calls, 0..10_000) :: 0..10_000
@spec flag(:sensitive, boolean) :: boolean
@spec flag(:trap_exit, boolean) :: boolean
defdelegate flag(flag, value), to: :erlang, as: :process_flag
@spec flag(process_flag, term) :: term
def flag(flag, value) do
:erlang.process_flag(flag, value)
end
@doc """
Sets the given `flag` to `value` for the given process `pid`.
Returns the old value of `flag`.
It raises `ArgumentError` if `pid` is not a local process.
The allowed values for `flag` are only a subset of those allowed in `flag/2`,
namely `:save_calls`.
Sets certain flags for the process `pid`, in the same manner as `flag/2`.
Returns the old value of the `flag`. The allowed values for `flag` are
only a subset of those allowed in `flag/2`, namely `:save_calls`.
See [`:erlang.process_flag/3`](http://www.erlang.org/doc/man/erlang.html#process_flag-3) for more info.
Inlined by the compiler.
"""
@spec flag(pid, :save_calls, 0..10_000) :: 0..10_000
defdelegate flag(pid, flag, value), to: :erlang, as: :process_flag
@spec flag(pid, :save_calls, non_neg_integer) :: non_neg_integer
def flag(pid, flag, value) do
:erlang.process_flag(pid, flag, value)
end
@doc """
Returns information about the process identified by `pid`, or returns `nil` if the process
is not alive.
Use this only for debugging information.
See [`:erlang.process_info/1`](http://www.erlang.org/doc/man/erlang.html#process_info-1) for more info.
"""
@spec info(pid) :: keyword
@spec info(pid) :: Keyword.t
def info(pid) do
nillify :erlang.process_info(pid)
end
@@ -641,9 +522,7 @@ defmodule Process do
end
@doc """
Puts the calling process into a "hibernation" state.
The calling process is put into a waiting state
Puts the calling process into a wait state
where its memory allocation has been reduced as much as possible,
which is useful if the process does not expect to receive any messages
in the near future.
@@ -653,7 +532,9 @@ defmodule Process do
Inlined by the compiler.
"""
@spec hibernate(module, atom, list) :: no_return
defdelegate hibernate(mod, fun_name, args), to: :erlang
def hibernate(mod, fun, args) do
:erlang.hibernate(mod, fun, args)
end
@compile {:inline, nillify: 1}
defp nillify(:undefined), do: nil
+47 -72
View File
@@ -23,9 +23,9 @@ defmodule Protocol do
:lists.seq(2, arity))
type_args = [quote(do: t) | type_args]
call_args = :lists.map(fn pos -> Macro.var(String.to_atom("var" <> Integer.to_string(pos)), __MODULE__) end,
call_args = :lists.map(fn i -> {String.to_atom(<<?x, i + 64>>), [], __MODULE__} end,
:lists.seq(2, arity))
call_args = [quote(do: term) | call_args]
call_args = [quote(do: t) | call_args]
quote do
name = unquote(name)
@@ -39,7 +39,7 @@ defmodule Protocol do
# Generate the actual implementation
Kernel.def unquote(name)(unquote_splicing(call_args)) do
impl_for!(term).unquote(name)(unquote_splicing(call_args))
impl_for!(t).unquote(name)(unquote_splicing(call_args))
end
# Convert the spec to callback if possible,
@@ -50,7 +50,7 @@ defmodule Protocol do
end
defmacro def(_) do
raise ArgumentError, "invalid arguments for def inside defprotocol"
raise ArgumentError, "invalid args for def inside defprotocol"
end
@doc """
@@ -179,7 +179,7 @@ defmodule Protocol do
prefix = Atom.to_charlist(protocol) ++ '.'
extract_matching_by_attribute paths, prefix, fn
_mod, attributes ->
case attributes[:protocol_impl] do
case attributes[:impl] do
[protocol: ^protocol, for: for] -> for
_ -> nil
end
@@ -254,22 +254,23 @@ defmodule Protocol do
{:error, :not_a_protocol} |
{:error, :no_beam_info}
def consolidate(protocol, types) when is_atom(protocol) do
with {:ok, ast_info, chunks_info} <- beam_protocol(protocol),
{:ok, code} <- change_debug_info(ast_info, types),
do: compile(protocol, code, chunks_info)
with {:ok, info} <- beam_protocol(protocol),
{:ok, code, docs} <- change_debug_info(info, types),
do: compile(code, docs)
end
@docs_chunk 'ExDc'
defp beam_protocol(protocol) do
chunk_ids = [:abstract_code, :attributes, :compile_info, 'ExDc']
chunk_ids = [:abstract_code, :attributes, @docs_chunk]
opts = [:allow_missing_chunks]
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
{:ok, {^protocol, [{:abstract_code, {_raw, abstract_code}},
{:attributes, attributes},
{:compile_info, compile_info},
{'ExDc', docs}]}} ->
{@docs_chunk, docs}]}} ->
case attributes[:protocol] do
[fallback_to_any: any] ->
{:ok, {protocol, any, abstract_code}, {compile_info, docs}}
{:ok, {protocol, any, abstract_code, docs}}
_ ->
{:error, :not_a_protocol}
end
@@ -287,33 +288,29 @@ defmodule Protocol do
# Change the debug information to the optimized
# impl_for/1 dispatch version.
defp change_debug_info({protocol, any, code}, types) do
defp change_debug_info({protocol, any, code, docs}, types) do
types = if any, do: types, else: List.delete(types, Any)
all = [Any] ++ for {_guard, mod} <- __builtin__(), do: mod
structs = types -- all
case change_impl_for(code, protocol, types, structs, false, []) do
{:ok, ret} -> {:ok, ret}
{:ok, ret} -> {:ok, ret, docs}
other -> other
end
end
defp change_impl_for([{:function, line, :__protocol__, 1, clauses} | tail], protocol, types, structs, _, acc) do
abstract_types = :erl_parse.abstract(:lists.usort(types))
defp change_impl_for([{:function, line, :__protocol__, 1, clauses} | t], protocol, types, structs, _, acc) do
clauses = :lists.map(fn
{:clause, l, [{:atom, _, :consolidated?}], [], [{:atom, _, _}]} ->
{:clause, l, [{:atom, 0, :consolidated?}], [], [{:atom, 0, true}]}
{:clause, l, [{:atom, _, :impls}], [], [{:atom, _, _}]} ->
{:clause, l, [{:atom, 0, :impls}], [], [{:tuple, 0, [{:atom, 0, :consolidated}, abstract_types]}]}
{:clause, _, _, _, _} = c ->
c
end, clauses)
change_impl_for(tail, protocol, types, structs, true,
change_impl_for(t, protocol, types, structs, true,
[{:function, line, :__protocol__, 1, clauses} | acc])
end
defp change_impl_for([{:function, line, :impl_for, 1, _} | tail], protocol, types, structs, protocol?, acc) do
defp change_impl_for([{:function, line, :impl_for, 1, _} | t], protocol, types, structs, is_protocol, acc) do
fallback = if Any in types, do: load_impl(protocol, Any)
clauses = for {guard, mod} <- __builtin__(),
@@ -323,45 +320,26 @@ defmodule Protocol do
clauses = [struct_clause_for(line) | clauses] ++
[fallback_clause_for(fallback, protocol, line)]
change_impl_for(tail, protocol, types, structs, protocol?,
change_impl_for(t, protocol, types, structs, is_protocol,
[{:function, line, :impl_for, 1, clauses} | acc])
end
defp change_impl_for([{:function, line, :struct_impl_for, 1, _} | tail], protocol, types, structs, protocol?, acc) do
defp change_impl_for([{:function, line, :struct_impl_for, 1, _} | t], protocol, types, structs, is_protocol, acc) do
fallback = if Any in types, do: load_impl(protocol, Any)
clauses = for struct <- structs, do: each_struct_clause_for(struct, protocol, line)
clauses = clauses ++ [fallback_clause_for(fallback, protocol, line)]
change_impl_for(tail, protocol, types, structs, protocol?,
change_impl_for(t, protocol, types, structs, is_protocol,
[{:function, line, :struct_impl_for, 1, clauses} | acc])
end
defp change_impl_for([{:attribute, line, :spec, {{:__protocol__, 1}, funspecs}} | tail], protocol, types, structs, protocol?, acc) do
new_specs = for spec <- funspecs do
case spec do
{:type, line, :fun, [{:type, _, :product, [{:atom, _, :consolidated?}]}, _]} ->
{:type, line, :fun,
[{:type, line, :product, [{:atom, 0, :consolidated?}]},
{:atom, 0, true}]}
{:type, line, :fun, [{:type, _, :product, [{:atom, _, :impls}]}, _]} ->
{:type, line, :fun,
[{:type, line, :product, [{:atom, 0, :impls}]},
{:type, 0, :tuple,
[{:atom, 0, :consolidated},
{:type, 0, :list, [{:type, 0, :module, []}]}]}]}
other -> other
end
end
change_impl_for(tail, protocol, types, structs, protocol?, [{:attribute, line, :spec, {{:__protocol__, 1}, new_specs}} | acc])
defp change_impl_for([h | t], protocol, info, types, is_protocol, acc) do
change_impl_for(t, protocol, info, types, is_protocol, [h | acc])
end
defp change_impl_for([head | tail], protocol, info, types, protocol?, acc) do
change_impl_for(tail, protocol, info, types, protocol?, [head | acc])
end
defp change_impl_for([], _protocol, _info, _types, protocol?, acc) do
if protocol? do
{:ok, Enum.reverse(acc)}
defp change_impl_for([], protocol, _info, _types, is_protocol, acc) do
if is_protocol do
{:ok, {protocol, Enum.reverse(acc)}}
else
{:error, :not_a_protocol}
end
@@ -391,9 +369,9 @@ defmodule Protocol do
[{:var, line, :x}]}]}
end
defp each_struct_clause_for(struct, protocol, line) do
{:clause, line, [{:atom, line, struct}], [],
[{:atom, line, load_impl(protocol, struct)}]}
defp each_struct_clause_for(other, protocol, line) do
{:clause, line, [{:atom, line, other}], [],
[{:atom, line, load_impl(protocol, other)}]}
end
defp fallback_clause_for(value, _protocol, line) do
@@ -406,14 +384,13 @@ defmodule Protocol do
end
# Finally compile the module and emit its bytecode.
defp compile(protocol, code, {compile_info, docs}) do
opts = Keyword.take(compile_info, [:source])
opts = if Code.compiler_options[:debug_info], do: [:debug_info | opts], else: opts
defp compile({protocol, code}, docs) do
opts = if Code.compiler_options[:debug_info], do: [:debug_info], else: []
{:ok, ^protocol, binary, _warnings} = :compile.forms(code, [:return | opts])
{:ok,
case docs do
:missing_chunk -> binary
_ -> :elixir_erl.add_beam_chunks(binary, [{"ExDc", docs}])
_ -> :elixir_module.add_beam_chunk(binary, @docs_chunk, docs)
end}
end
@@ -443,7 +420,7 @@ defmodule Protocol do
_ = unquote(block)
# Finalize expansion
unquote(after_defprotocol())
unquote(after_defprotocol)
end
end
end
@@ -462,14 +439,14 @@ defmodule Protocol do
struct_impl_for(struct)
end
# Define the implementation for built-ins
# Define the implementation for builtins.
:lists.foreach(fn {guard, mod} ->
target = Module.concat(__MODULE__, mod)
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
case impl_for?(unquote(target)) do
true -> unquote(target).__impl__(:target)
false -> any_impl_for()
false -> any_impl_for
end
end
end, builtin)
@@ -492,9 +469,9 @@ defmodule Protocol do
# Internal handler for Any
if @fallback_to_any do
Kernel.defp any_impl_for(), do: __MODULE__.Any.__impl__(:target)
Kernel.defp any_impl_for, do: __MODULE__.Any.__impl__(:target)
else
Kernel.defp any_impl_for(), do: nil
Kernel.defp any_impl_for, do: nil
end
# Internal handler for Structs
@@ -502,7 +479,7 @@ defmodule Protocol do
target = Module.concat(__MODULE__, struct)
case impl_for?(target) do
true -> target.__impl__(:target)
false -> any_impl_for()
false -> any_impl_for
end
end
@@ -527,20 +504,18 @@ defmodule Protocol do
@doc false
@spec __protocol__(:module) :: __MODULE__
@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@functions))
@spec __protocol__(:consolidated?) :: false
@spec __protocol__(:impls) :: :not_consolidated
@spec __protocol__(:consolidated?) :: boolean
Kernel.def __protocol__(:module), do: __MODULE__
Kernel.def __protocol__(:functions), do: unquote(:lists.sort(@functions))
Kernel.def __protocol__(:consolidated?), do: false
Kernel.def __protocol__(:impls), do: :not_consolidated
end
end
@doc false
def __functions_spec__([]),
do: []
def __functions_spec__([head | tail]),
do: [:lists.foldl(&{:|, [], [&1, &2]}, head, tail), quote(do: ...)]
def __functions_spec__([h | t]),
do: [:lists.foldl(&{:|, [], [&1, &2]}, h, t), quote(do: ...)]
@doc false
def __impl__(protocol, opts) do
@@ -581,8 +556,8 @@ defmodule Protocol do
unquote(block)
Module.register_attribute(__MODULE__, :protocol_impl, persist: true)
@protocol_impl [protocol: @protocol, for: @for]
Module.register_attribute(__MODULE__, :impl, persist: true)
@impl [protocol: @protocol, for: @for]
unquote(impl)
end
@@ -626,8 +601,8 @@ defmodule Protocol do
apply(mod, fun, args)
else
Module.create(Module.concat(protocol, for), quote do
Module.register_attribute(__MODULE__, :protocol_impl, persist: true)
@protocol_impl [protocol: unquote(protocol), for: unquote(for)]
Module.register_attribute(__MODULE__, :impl, persist: true)
@impl [protocol: unquote(protocol), for: unquote(for)]
@doc false
@spec __impl__(:target) :: unquote(impl)
@@ -658,9 +633,9 @@ defmodule Protocol do
specs = Module.get_attribute(module, :spec)
found =
:lists.map(fn {:spec, expr, pos} ->
:lists.map(fn {:spec, expr, caller} ->
if Kernel.Typespec.spec_to_signature(expr) == signature do
Module.store_typespec(module, :callback, {:callback, expr, pos})
Kernel.Typespec.define_spec(:callback, expr, caller)
true
end
end, specs)
+14 -15
View File
@@ -5,13 +5,13 @@ defmodule Range do
A range represents a discrete number of values where
the first and last values are integers.
Ranges can be either increasing (`first <= last`) or
decreasing (`first > last`). Ranges are also always
Ranges can be either increasing (first <= last) or
decreasing (first > last). Ranges are also always
inclusive.
A range is represented internally as a struct. However,
A Range is represented internally as a struct. However,
the most common form of creating and matching on ranges
is via the `../2` macro, auto-imported from `Kernel`:
is via the `../2` macro, auto-imported from Kernel:
iex> range = 1..3
1..3
@@ -21,9 +21,8 @@ defmodule Range do
iex> last
3
A range implements the `Enumerable` protocol, which means
functions in the `Enum` module can be used to work with
ranges:
A Range implements the Enumerable protocol, which means
all of the functions in the Enum module is available:
iex> range = 1..10
1..10
@@ -78,23 +77,23 @@ end
defimpl Enumerable, for: Range do
def reduce(first..last, acc, fun) do
reduce(first, last, acc, fun, _up? = last >= first)
reduce(first, last, acc, fun, last >= first)
end
defp reduce(_x, _y, {:halt, acc}, _fun, _up?) do
defp reduce(_x, _y, {:halt, acc}, _fun, _up) do
{:halted, acc}
end
defp reduce(x, y, {:suspend, acc}, fun, up?) do
{:suspended, acc, &reduce(x, y, &1, fun, up?)}
defp reduce(x, y, {:suspend, acc}, fun, up) do
{:suspended, acc, &reduce(x, y, &1, fun, up)}
end
defp reduce(x, y, {:cont, acc}, fun, _up? = true) when x <= y do
reduce(x + 1, y, fun.(x, acc), fun, _up? = true)
defp reduce(x, y, {:cont, acc}, fun, true) when x <= y do
reduce(x + 1, y, fun.(x, acc), fun, true)
end
defp reduce(x, y, {:cont, acc}, fun, _up? = false) when x >= y do
reduce(x - 1, y, fun.(x, acc), fun, _up? = false)
defp reduce(x, y, {:cont, acc}, fun, false) when x >= y do
reduce(x - 1, y, fun.(x, acc), fun, false)
end
defp reduce(_, _, {:cont, acc}, _fun, _up) do
+57 -128
View File
@@ -1,6 +1,6 @@
defmodule Record do
@moduledoc """
Module to work with, define, and import records.
Module to work with, define and import records.
Records are simply tuples where the first element is an atom:
@@ -17,14 +17,15 @@ defmodule Record do
1. to work with short, internal data
2. to interface with Erlang records
The macros `defrecord/3` and `defrecordp/3` can be used to create records
while `extract/2` and `extract_all/1` can be used to extract records from
Erlang files.
The macros `defrecord/3` and `defrecordp/3` can be used to create
records while `extract/2` can be used to extract records from Erlang
files.
## Types
Types can be defined for tuples with the `record/2` macro (only available in
typespecs). This macro will expand to a tuple as seen in the example below:
Types can be defined for tuples with the `record/2` macro (only available
in typespecs). Like with the generated record macros it will expand to
a tuple.
defmodule MyModule do
require Record
@@ -33,34 +34,14 @@ defmodule Record do
@type user :: record(:user, name: String.t, age: integer)
# expands to: "@type user :: {:user, String.t, integer}"
end
"""
@doc """
Extracts record information from an Erlang file.
Returns a quoted expression containing the fields as a list
of tuples.
`name`, which is the name of the extracted record, is expected to be an atom
*at compile time*.
## Options
This function accepts the following options, which are exclusive to each other
(i.e., only one of them can be used in the same call):
* `:from` - (binary representing a path to a file) path to the Erlang file
that contains the record definition to extract; with this option, this
function uses the same path lookup used by the `-include` attribute used in
Erlang modules.
* `:from_lib` - (binary representing a path to a file) path to the Erlang
file that contains the record definition to extract; with this option,
this function uses the same path lookup used by the `-include_lib`
attribute used in Erlang modules.
These options are expected to be literals (including the binary values) at
compile time.
of tuples. It expects the record name to be an atom and the
library path to be a string at expansion time.
## Examples
@@ -71,7 +52,6 @@ defmodule Record do
uid: :undefined, gid: :undefined]
"""
@spec extract(name :: atom, keyword) :: keyword
def extract(name, opts) when is_atom(name) and is_list(opts) do
Record.Extractor.extract(name, opts)
end
@@ -79,34 +59,18 @@ defmodule Record do
@doc """
Extracts all records information from an Erlang file.
Returns a keyword list of `{record_name, fields}` tuples where `record_name`
is the name of an extracted record and `fields` is a list of `{field, value}`
tuples representing the fields for that record.
Returns a keyword list containing extracted record names as keys, and
lists of tuples describing the fields as values. It expects a named
argument :from or :from_lib, which correspond to *include* or
*include_lib* attribute from Erlang modules, respectively.
## Options
This function accepts the following options, which are exclusive to each other
(i.e., only one of them can be used in the same call):
* `:from` - (binary representing a path to a file) path to the Erlang file
that contains the record definitions to extract; with this option, this
function uses the same path lookup used by the `-include` attribute used in
Erlang modules.
* `:from_lib` - (binary representing a path to a file) path to the Erlang
file that contains the record definitions to extract; with this option,
this function uses the same path lookup used by the `-include_lib`
attribute used in Erlang modules.
These options are expected to be literals (including the binary values) at
compile time.
"""
@spec extract_all(keyword) :: [{name :: atom, keyword}]
def extract_all(opts) when is_list(opts) do
Record.Extractor.extract_all(opts)
end
@doc """
Checks if the given `data` is a record of kind `kind`.
Checks if the given `data` is a record of `kind`.
This is implemented as a macro so it can be used in guard clauses.
@@ -164,27 +128,11 @@ defmodule Record do
end
@doc """
Defines a set of macros to create, access, and pattern match
on a record.
Defines a set of macros to create and access a record.
The name of the generated macros will be `name` (which has to be an
atom). `tag` is also an atom and is used as the "tag" for the record (i.e.,
the first element of the record tuple); by default (if `nil`), it's the same
as `name`. `kv` is a keyword list of `name: default_value` fields for the
new record.
The following macros are generated:
* `name/0` to create a new record with default values for all fields
* `name/1` to create a new record with the given fields and values,
to get the zero-based index of the given field in a record or to
convert the given record to a keyword list
* `name/2` to update an existing record with the given fields and values
or to access a given field in a given record
All these macros are public macros (as defined by `defmacro`).
See the "Examples" section for examples on how to use these macros.
The macros are going to have `name`, a tag (which defaults)
to the name if none is given, and a set of fields given by
`kv`.
## Examples
@@ -194,10 +142,7 @@ defmodule Record do
end
In the example above, a set of macros named `user` but with different
arities will be defined to manipulate the underlying record.
# Import the module to make the user macros locally available
import User
arities will be defined to manipulate the underlying record:
# To create records
record = user() #=> {:user, "meg", 25}
@@ -209,10 +154,6 @@ defmodule Record do
# To update the record
user(record, age: 26) #=> {:user, "meg", 26}
# To get the zero-based index of the field in record tuple
# (index 0 is occupied by the record "tag")
user(:name) #=> 1
# Convert a record to a keyword list
user(record) #=> [name: "meg", age: 26]
@@ -224,24 +165,23 @@ defmodule Record do
user(name: name) = record
name #=> "meg"
By default, Elixir uses the record name as the first element of the tuple (the "tag").
However, a different tag can be specified when defining a record,
as in the following example, in which we use `Customer` as the second argument of `defrecord/3`:
By default, Elixir uses the record name as the first element of
the tuple (the tag). But it can be changed to something else:
defmodule User do
require Record
Record.defrecord :user, Customer, name: nil
Record.defrecord :user, User, name: nil
end
require User
User.user() #=> {Customer, nil}
User.user() #=> {User, nil}
## Defining extracted records with anonymous functions in the values
## Defining extracted records with anonymous functions
If a record defines an anonymous function in the default values, an
`ArgumentError` will be raised. This can happen unintentionally when defining
a record after extracting it from an Erlang library that uses anonymous
functions for defaults.
If a record defines an anonymous function, an `ArgumentError`
will occur if you attempt to create a record with it.
This can occur unintentionally when defining a record after extracting
it from an Erlang library that uses anonymous functions for defaults.
Record.defrecord :my_rec, Record.extract(...)
#=> ** (ArgumentError) invalid value for record field fun_field,
@@ -255,7 +195,6 @@ defmodule Record do
Record.defrecord :my_rec, Record.extract(...) |> Keyword.merge(fun_field: &__MODULE__.foo/2)
def foo(bar, baz), do: IO.inspect({bar, baz})
end
"""
defmacro defrecord(name, tag \\ nil, kv) do
quote bind_quoted: [name: name, tag: tag, kv: kv] do
@@ -294,14 +233,14 @@ defmodule Record do
@doc false
def __fields__(type, fields) do
:lists.map(fn
{key, value} when is_atom(key) ->
{key, val} when is_atom(key) ->
try do
Macro.escape(value)
Macro.escape(val)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for record field #{key}, " <> Exception.message(e)
else
value -> {key, value}
val -> {key, val}
end
key when is_atom(key) ->
{key, nil}
@@ -312,51 +251,50 @@ defmodule Record do
# Callback invoked from record/0 and record/1 macros.
@doc false
def __access__(tag, fields, args, caller) do
def __access__(atom, fields, args, caller) do
cond do
is_atom(args) ->
index(tag, fields, args)
index(atom, fields, args)
Keyword.keyword?(args) ->
create(tag, fields, args, caller)
create(atom, fields, args, caller)
true ->
fields = Macro.escape(fields)
case Macro.expand(args, caller) do
{:{}, _, [^tag | list]} when length(list) == length(fields) ->
record = List.to_tuple([tag | list])
Record.__keyword__(tag, fields, record)
{^tag, arg} when length(fields) == 1 ->
Record.__keyword__(tag, fields, {tag, arg})
{:{}, _, [^atom | list]} when length(list) == length(fields) ->
record = List.to_tuple([atom | list])
Macro.escape(Record.__keyword__(atom, fields, record))
{^atom, arg} when length(fields) == 1 ->
Macro.escape(Record.__keyword__(atom, fields, {atom, arg}))
_ ->
quote do: Record.__keyword__(unquote(tag), unquote(fields), unquote(args))
quote do: Record.__keyword__(unquote(atom), unquote(fields), unquote(args))
end
end
end
# Callback invoked from the record/2 macro.
@doc false
def __access__(tag, fields, record, args, caller) do
def __access__(atom, fields, record, args, caller) do
cond do
is_atom(args) ->
get(tag, fields, record, args)
get(atom, fields, record, args)
Keyword.keyword?(args) ->
update(tag, fields, record, args, caller)
update(atom, fields, record, args, caller)
true ->
msg = "expected arguments to be a compile time atom or a keyword list, got: #{Macro.to_string args}"
msg = "expected arguments to be a compile time atom or keywords, got: #{Macro.to_string args}"
raise ArgumentError, msg
end
end
# Gets the index of field.
defp index(tag, fields, field) do
defp index(atom, fields, field) do
if index = find_index(fields, field, 0) do
index - 1 # Convert to Elixir index
else
raise ArgumentError, "record #{inspect tag} does not have the key: #{inspect field}"
raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect field}"
end
end
# Creates a new record with the given default fields and keyword values.
defp create(tag, fields, keyword, caller) do
defp create(atom, fields, keyword, caller) do
in_match = Macro.Env.in_match?(caller)
keyword = apply_underscore(fields, keyword)
@@ -374,15 +312,15 @@ defmodule Record do
case remaining do
[] ->
{:{}, [], [tag | match]}
{:{}, [], [atom | match]}
_ ->
keys = for {key, _} <- remaining, do: key
raise ArgumentError, "record #{inspect tag} does not have the key: #{inspect hd(keys)}"
raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect hd(keys)}"
end
end
# Updates a record given by var with the given keyword.
defp update(tag, fields, var, keyword, caller) do
defp update(atom, fields, var, keyword, caller) do
if Macro.Env.in_match?(caller) do
raise ArgumentError, "cannot invoke update style macro inside match"
end
@@ -396,20 +334,20 @@ defmodule Record do
:erlang.setelement(unquote(index), unquote(acc), unquote(value))
end
else
raise ArgumentError, "record #{inspect tag} does not have the key: #{inspect key}"
raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect key}"
end
end
end
# Gets a record key from the given var.
defp get(tag, fields, var, key) do
defp get(atom, fields, var, key) do
index = find_index(fields, key, 0)
if index do
quote do
:erlang.element(unquote(index), unquote(var))
end
else
raise ArgumentError, "record #{inspect tag} does not have the key: #{inspect key}"
raise ArgumentError, "record #{inspect atom} does not have the key: #{inspect key}"
end
end
@@ -419,29 +357,20 @@ defmodule Record do
# Returns a keyword list of the record
@doc false
def __keyword__(tag, fields, record) do
if is_record(record, tag) do
def __keyword__(atom, fields, record) do
if is_record(record, atom) do
[_tag | values] = Tuple.to_list(record)
case join_keyword(fields, values, []) do
kv when is_list(kv) ->
kv
expected_fields ->
msg = "expected argument to be a #{inspect tag} record with #{expected_fields} fields, got: #{inspect record}"
raise ArgumentError, msg
end
join_keyword(fields, values, [])
else
msg = "expected argument to be a literal atom, literal keyword or a #{inspect tag} record, got runtime: #{inspect record}"
msg = "expected argument to be a literal atom, literal keyword or a #{inspect atom} record, got runtime: #{inspect record}"
raise ArgumentError, msg
end
end
# Returns a keyword list, or expected number of fields on size mismatch
defp join_keyword([{field, _default} | fields], [value | values], acc),
do: join_keyword(fields, values, [{field, value} | acc])
defp join_keyword([], [], acc),
do: :lists.reverse(acc)
defp join_keyword(rest_fields, _rest_values, acc),
do: length(acc) + length(rest_fields) # expected fields
defp apply_underscore(fields, keyword) do
case Keyword.fetch(keyword, :_) do
+38 -135
View File
@@ -1,13 +1,14 @@
defmodule Regex do
@moduledoc ~S"""
Provides regular expressions for Elixir.
Provides regular expressions for Elixir. Built on top of Erlang's `:re`
module.
Regex is based on PCRE (Perl Compatible Regular Expressions) and
built on top of Erlang's `:re` module. More information can be found
in the [`:re` module documentation](http://www.erlang.org/doc/man/re.html).
As the `:re` module, Regex is based on PCRE
(Perl Compatible Regular Expressions). More information can be
found in the [`:re` module documentation](http://www.erlang.org/doc/man/re.html).
Regular expressions in Elixir can be created using the sigils
[`~r`](Kernel.html#sigil_r/2) or [`~R`](Kernel.html#sigil_R/2):
Regular expressions in Elixir can be created using `Regex.compile!/2`
or using the special form with [`~r`](Kernel.html#sigil_r/2) or [`~R`](Kernel.html#sigil_R/2):
# A simple regular expressions that matches foo anywhere in the string
~r/foo/
@@ -15,36 +16,8 @@ defmodule Regex do
# A regular expression with case insensitive and Unicode options
~r/foo/iu
Regular expressions created via sigils are pre-compiled and stored
in the `.beam` file. Notice this may be a problem if you are precompiling
Elixir, see the "Precompilation" section for more information.
A Regex is represented internally as the `Regex` struct. Therefore,
`%Regex{}` can be used whenever there is a need to match on them.
Keep in mind it is not guaranteed two regular expressions from the
same source are equal, for example:
~r/(?<foo>.)(?<bar>.)/ == ~r/(?<foo>.)(?<bar>.)/
may return `true` or `false` depending on your machine, endianess,
available optimizations and others. You can, however, retrieve the source
of a compiled regular expression by accessing the `source` field, and then
compare those directly:
~r/(?<foo>.)(?<bar>.)/.source == ~r/(?<foo>.)(?<bar>.)/.source
## Precompilation
Regular expressions built with sigil are precompiled and stored in `.beam`
files. This may be a problem if you are precompiling Elixir to run in
different OTP releases, as OTP releases may update the underlying regular
expression engine at any time.
For such reasons, we always recomend precompiling Elixir projects using
the OTP version meant to run in production. In case cross-compilation is
really necessary, you can manually invoke `Regex.recompile/1` or `Regex.
recompile!/1` to perform a runtime version check and recompile the regex
if necessary.
## Modifiers
@@ -54,7 +27,7 @@ defmodule Regex do
modifiers like `\w`, `\W`, `\s` and friends to also match on Unicode.
It expects valid Unicode strings to be given on match
* `caseless` (i) - adds case insensitivity
* `caseless` (i) - add case insensitivity
* `dotall` (s) - causes dot to match newlines and also set newline to
anycrlf; the new line setting can be overridden by setting `(*CR)` or
@@ -97,7 +70,7 @@ defmodule Regex do
explicitly captured subpatterns, but not the complete matching part of
the string
* `:none` - does not return matching subpatterns at all
* `:none` - do not return matching subpatterns at all
* `:all_names` - captures all names in the Regex
@@ -105,7 +78,7 @@ defmodule Regex do
"""
defstruct re_pattern: nil, source: "", opts: "", re_version: ""
defstruct re_pattern: nil, source: "", opts: ""
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary}
@@ -118,7 +91,7 @@ defmodule Regex do
The given options can either be a binary with the characters
representing the same regex options given to the `~r` sigil,
or a list of options, as expected by the Erlang's `:re` module.
or a list of options, as expected by the Erlang's [`:re` module](http://www.erlang.org/doc/man/re.html).
It returns `{:ok, regex}` in case of success,
`{:error, reason}` otherwise.
@@ -133,84 +106,40 @@ defmodule Regex do
"""
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, any}
def compile(source, options \\ "") do
compile(source, options, version())
end
def compile(source, options \\ "")
defp compile(source, options, version) when is_binary(options) do
def compile(source, options) when is_binary(options) do
case translate_options(options, []) do
{:error, rest} ->
{:error, {:invalid_option, rest}}
translated_options ->
compile(source, translated_options, options, version)
compile(source, translated_options, options)
end
end
defp compile(source, options, version) when is_list(options) do
compile(source, options, "", version)
def compile(source, options) when is_list(options) do
compile(source, options, "")
end
defp compile(source, opts, doc_opts, version) when is_binary(source) do
defp compile(source, opts, doc_opts) when is_binary(source) do
case :re.compile(source, opts) do
{:ok, re_pattern} ->
{:ok, %Regex{re_pattern: re_pattern, re_version: version, source: source, opts: doc_opts}}
{:ok, %Regex{re_pattern: re_pattern, source: source, opts: doc_opts}}
error ->
error
end
end
@doc """
Compiles the regular expression and raises `Regex.CompileError` in case of errors.
Compiles the regular expression according to the given options.
Fails with `Regex.CompileError` if the regex cannot be compiled.
"""
@spec compile!(binary, binary | [term]) :: t
def compile!(source, options \\ "") do
case compile(source, options) do
{:ok, regex} -> regex
{:error, {reason, at}} -> raise Regex.CompileError, "#{reason} at position #{at}"
end
end
@doc """
Recompiles the existing regular expression if necessary.
This checks the version stored in the regular expression
and recompiles the regex in case of version mismatch.
"""
@spec recompile(t) :: t
def recompile(%Regex{} = regex) do
version = version()
# We use Map.get/3 by choice to support old regexes versions.
case Map.get(regex, :re_version, :error) do
^version ->
{:ok, regex}
_ ->
%{source: source, opts: opts} = regex
compile(source, opts, version)
end
end
@doc """
Recompiles the existing regular expression and raises `Regex.CompileError` in case of errors.
"""
@spec recompile!(t) :: t
def recompile!(regex) do
case recompile(regex) do
{:ok, regex} -> regex
{:error, {reason, at}} -> raise Regex.CompileError, "#{reason} at position #{at}"
end
end
@doc """
Returns the version of the underlying Regex engine.
"""
# TODO: No longer check for function_exported? on OTP 20+.
def version do
if function_exported?(:re, :version, 0) do
:re.version()
else
"8.33 2013-05-29"
{:error, {reason, at}} -> raise Regex.CompileError, message: "#{reason} at position #{at}"
end
end
@@ -255,7 +184,7 @@ defmodule Regex do
## Options
* `:return` - sets to `:index` to return indexes. Defaults to `:binary`.
* `:return` - set to `:index` to return indexes. Defaults to `:binary`.
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
to see the possible capture values.
@@ -361,7 +290,7 @@ defmodule Regex do
names
end
@doc ~S"""
@doc """
Same as `run/3`, but scans the target several times collecting all
matches of the regular expression.
@@ -370,7 +299,7 @@ defmodule Regex do
## Options
* `:return` - sets to `:index` to return indexes. Defaults to `:binary`.
* `:return` - set to `:index` to return indexes. Defaults to `:binary`.
* `:capture` - what to capture in the result. Check the moduledoc for `Regex`
to see the possible capture values.
@@ -385,9 +314,6 @@ defmodule Regex do
iex> Regex.scan(~r/e/, "abcd")
[]
iex> Regex.scan(~r/\p{Sc}/u, "$, £, and €")
[["$"], ["£"], ["€"]]
"""
@spec scan(t, String.t, [term]) :: [[String.t]]
def scan(regex, string, options \\ [])
@@ -426,28 +352,28 @@ defmodule Regex do
## Examples
iex> Regex.split(~r{-}, "a-b-c")
iex> Regex.split(~r/-/, "a-b-c")
["a", "b", "c"]
iex> Regex.split(~r{-}, "a-b-c", [parts: 2])
iex> Regex.split(~r/-/, "a-b-c", [parts: 2])
["a", "b-c"]
iex> Regex.split(~r{-}, "abc")
iex> Regex.split(~r/-/, "abc")
["abc"]
iex> Regex.split(~r{}, "abc")
iex> Regex.split(~r//, "abc")
["a", "b", "c", ""]
iex> Regex.split(~r{a(?<second>b)c}, "abc")
iex> Regex.split(~r/a(?<second>b)c/, "abc")
["", ""]
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second])
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second])
["a", "c"]
iex> Regex.split(~r{(x)}, "Elixir", include_captures: true)
iex> Regex.split(~r/(x)/, "Elixir", include_captures: true)
["Eli", "x", "ir"]
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second], include_captures: true)
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second], include_captures: true)
["a", "b", "c"]
"""
@@ -530,10 +456,8 @@ defmodule Regex do
The replacement can be either a string or a function. The string
is used as a replacement for every match and it allows specific
captures to be accessed via `\N` or `\g{N}`, where `N` is the
capture. In case `\0` is used, the whole match is inserted. Note
that in regexes the backslash needs to be escaped, hence in practice
you'll need to use `\\N` and `\\g{N}`.
captures to be accessed via `\\N` or `\g{N}`, where `N` is the
capture. In case `\\0` is used, the whole match is inserted.
When the replacement is a function, the function may have arity
N where each argument maps to a capture, with the first argument
@@ -700,6 +624,9 @@ defmodule Regex do
[get_index(string, h) | get_indexes(string, t, arity - 1)]
end
{:ok, pattern} = :re.compile(~S"[.^$*+?()\[\]{}\\\|\s#]", [:unicode])
@escape_pattern pattern
@doc ~S"""
Escapes a string to be literally matched in a regex.
@@ -714,31 +641,7 @@ defmodule Regex do
"""
@spec escape(String.t) :: String.t
def escape(string) when is_binary(string) do
string
|> escape(_length = 0, string)
|> IO.iodata_to_binary
end
@escapable '.^$*+?()[]{}|#-\\\t\n\v\f\r\s'
defp escape(<<char, rest::binary>>, length, original) when char in @escapable do
escape_char(rest, length, original, char)
end
defp escape(<<_, rest::binary>>, length, original) do
escape(rest, length + 1, original)
end
defp escape(<<>>, _length, original) do
original
end
defp escape_char(<<rest::binary>>, 0, _original, char) do
[?\\, char | escape(rest, 0, rest)]
end
defp escape_char(<<rest::binary>>, length, original, char) do
[binary_part(original, 0, length), ?\\, char | escape(rest, 0, rest)]
:re.replace(string, @escape_pattern, "\\\\&", [:global, {:return, :binary}])
end
# Helpers
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -6,12 +6,10 @@ defmodule Set do
"""
@type value :: any
@type values :: [value]
@type values :: [ value ]
@type t :: map
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate every function by 1.4
defmacrop target(set) do
quote do
case unquote(set) do
+165 -314
View File
@@ -85,7 +85,7 @@ defmodule Stream do
Note the functions in this module are guaranteed to return enumerables.
Since enumerables can have different shapes (structs, anonymous functions,
and so on), the functions in this module may return any of those shapes
and this may change at any time. For example, a function that today
and that this may change at any time. For example, a function that today
returns an anonymous function may return a struct in future releases.
"""
@@ -104,80 +104,84 @@ defmodule Stream do
{:cont, acc}
end
defmacrop next(fun, entry, acc) do
quote do: unquote(fun).(unquote(entry), unquote(acc))
defmacrop next(f, entry, acc) do
quote do: unquote(f).(unquote(entry), unquote(acc))
end
defmacrop acc(head, state, tail) do
quote do: [unquote(head), unquote(state) | unquote(tail)]
defmacrop acc(h, n, t) do
quote do: [unquote(h), unquote(n) | unquote(t)]
end
defmacrop next_with_acc(fun, entry, head, state, tail) do
defmacrop next_with_acc(f, entry, h, n, t) do
quote do
{reason, [head | tail]} = unquote(fun).(unquote(entry), [unquote(head) | unquote(tail)])
{reason, [head, unquote(state) | tail]}
{reason, [h | t]} = unquote(f).(unquote(entry), [unquote(h) | unquote(t)])
{reason, [h, unquote(n) | t]}
end
end
## Transformers
# Deprecate on v1.7
@doc false
@doc """
Shortcut to `chunk(enum, n, n)`.
"""
@spec chunk(Enumerable.t, non_neg_integer) :: Enumerable.t
def chunk(enum, n), do: chunk(enum, n, n, nil)
# Deprecate on v1.7
@doc false
def chunk(enum, n, step, leftover \\ nil)
when is_integer(n) and n > 0 and is_integer(step) and step > 0 do
chunk_every(enum, n, step, leftover || :discard)
end
@doc """
Shortcut to `chunk_every(enum, count, count)`.
"""
@spec chunk_every(Enumerable.t, pos_integer) :: Enumerable.t
def chunk_every(enum, count), do: chunk_every(enum, count, count, [])
Streams the enumerable in chunks, containing `n` items each, where
each new chunk starts `step` elements into the enumerable.
@doc """
Streams the enumerable in chunks, containing `count` items each,
where each new chunk starts `step` elements into the enumerable.
`step` is optional and, if not passed, defaults to `count`, i.e.
chunks do not overlap.
If the last chunk does not have `count` elements to fill the chunk,
elements are taken from `leftover` to fill in the chunk. If `leftover`
does not have enough elements to fill the chunk, then a partial chunk
is returned with less than `count` elements.
If `:discard` is given in `leftover`, the last chunk is discarded
unless it has exactly `count` elements.
`step` is optional and, if not passed, defaults to `n`, i.e.
chunks do not overlap. If the final chunk does not have `n`
elements to fill the chunk, elements are taken as necessary
from `leftover` if it was passed. If `leftover` is passed and
does not have enough elements to fill the chunk, then the chunk is
returned anyway with less than `n` elements. If `leftover` is not
passed at all or is `nil`, then the partial chunk is discarded
from the result.
## Examples
iex> Stream.chunk_every([1, 2, 3, 4, 5, 6], 2) |> Enum.to_list
iex> Stream.chunk([1, 2, 3, 4, 5, 6], 2) |> Enum.to_list
[[1, 2], [3, 4], [5, 6]]
iex> Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, :discard) |> Enum.to_list
iex> Stream.chunk([1, 2, 3, 4, 5, 6], 3, 2) |> Enum.to_list
[[1, 2, 3], [3, 4, 5]]
iex> Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, [7]) |> Enum.to_list
iex> Stream.chunk([1, 2, 3, 4, 5, 6], 3, 2, [7]) |> Enum.to_list
[[1, 2, 3], [3, 4, 5], [5, 6, 7]]
iex> Stream.chunk_every([1, 2, 3, 4, 5, 6], 3, 3, []) |> Enum.to_list
iex> Stream.chunk([1, 2, 3, 4, 5, 6], 3, 3, []) |> Enum.to_list
[[1, 2, 3], [4, 5, 6]]
"""
@spec chunk_every(Enumerable.t, pos_integer, pos_integer, Enumerable.t | :discard) :: Enumerable.t
def chunk_every(enum, count, step, leftover \\ [])
when is_integer(count) and count > 0 and is_integer(step) and step > 0 do
R.chunk_every(&chunk_while/4, enum, count, step, leftover)
@spec chunk(Enumerable.t, pos_integer, pos_integer) :: Enumerable.t
@spec chunk(Enumerable.t, pos_integer, pos_integer, Enumerable.t | nil) :: Enumerable.t
def chunk(enum, n, step, leftover \\ nil)
when is_integer(n) and n > 0 and is_integer(step) and step > 0 do
limit = :erlang.max(n, step)
if is_nil(leftover) do
lazy enum, {[], 0}, fn(f1) -> R.chunk(n, step, limit, f1) end
else
lazy enum, {[], 0},
fn(f1) -> R.chunk(n, step, limit, f1) end,
&do_chunk(&1, n, leftover, &2)
end
end
defp do_chunk(acc(_, {_, 0}, _) = acc, _, _, _) do
{:cont, acc}
end
defp do_chunk(acc(h, {buffer, count} = old, t), n, leftover, f1) do
buffer = :lists.reverse(buffer, Enum.take(leftover, n - count))
next_with_acc(f1, buffer, h, old, t)
end
@doc """
Chunks the `enum` by buffering elements for which `fun` returns the same value.
Elements are only emitted when `fun` returns a new value or the `enum` finishes.
Chunks the `enum` by buffering elements for which `fun` returns
the same value and only emit them when `fun` returns a new value
or the `enum` finishes.
## Examples
@@ -188,54 +192,20 @@ defmodule Stream do
"""
@spec chunk_by(Enumerable.t, (element -> any)) :: Enumerable.t
def chunk_by(enum, fun) do
R.chunk_by(&chunk_while/4, enum, fun)
lazy enum, nil,
fn(f1) -> R.chunk_by(fun, f1) end,
&do_chunk_by(&1, &2)
end
@doc """
Chunks the `enum` with fine grained control when every chunk is emitted.
`chunk_fun` receives the current element and the accumulator and
must return `{:cont, element, acc}` to emit the given chunk and
continue with accumulator or `{:cont, acc}` to not emit any chunk
and continue with the return accumulator.
`after_fun` is invoked when iteration is done and must also return
`{:cont, element, acc}` or `{:cont, acc}`.
## Examples
iex> chunk_fun = fn i, acc ->
...> if rem(i, 2) == 0 do
...> {:cont, Enum.reverse([i | acc]), []}
...> else
...> {:cont, [i | acc]}
...> end
...> end
iex> after_fun = fn
...> [] -> {:cont, []}
...> acc -> {:cont, Enum.reverse(acc), []}
...> end
iex> stream = Stream.chunk_while(1..10, [], chunk_fun, after_fun)
iex> Enum.to_list(stream)
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]
"""
@spec chunk_while(Enumerable.t, acc,
(element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}),
(acc -> {:cont, chunk, acc} | {:cont, acc})) :: Enumerable.t when chunk: any
def chunk_while(enum, acc, chunk_fun, after_fun) do
lazy enum, acc,
fn(f1) -> R.chunk_while(chunk_fun, f1) end,
&after_chunk_while(&1, &2, after_fun)
defp do_chunk_by(acc(_, nil, _) = acc, _f1) do
{:cont, acc}
end
defp after_chunk_while(acc(h, acc, t), f1, after_fun) do
case after_fun.(acc) do
{:cont, emit, acc} -> next_with_acc(f1, emit, h, acc, t)
{:cont, acc} -> {:cont, acc(h, acc, t)}
end
defp do_chunk_by(acc(h, {buffer, _}, t), f1) do
next_with_acc(f1, :lists.reverse(buffer), h, nil, t)
end
@doc """
Creates a stream that only emits elements if they are different from the last emitted element.
@@ -265,7 +235,7 @@ defmodule Stream do
"""
@spec dedup_by(Enumerable.t, (element -> term)) :: Enumerable.t
def dedup_by(enum, fun) do
def dedup_by(enum, fun) when is_function(fun, 1) do
lazy enum, nil, fn f1 -> R.dedup(fun, f1) end
end
@@ -322,7 +292,7 @@ defmodule Stream do
The first item is always dropped, unless `nth` is 0.
`nth` must be a non-negative integer.
`nth` must be a non-negative integer, or `FunctionClauseError` will be thrown.
## Examples
@@ -340,7 +310,6 @@ defmodule Stream do
"""
@spec drop_every(Enumerable.t, non_neg_integer) :: Enumerable.t
def drop_every(enum, nth)
def drop_every(enum, 0), do: %Stream{enum: enum}
def drop_every([], _nth), do: %Stream{enum: []}
@@ -371,7 +340,7 @@ defmodule Stream do
## Examples
iex> stream = Stream.each([1, 2, 3], fn(x) -> send self(), x end)
iex> stream = Stream.each([1, 2, 3], fn(x) -> send self, x end)
iex> Enum.to_list(stream)
iex> receive do: (x when is_integer(x) -> x)
1
@@ -392,10 +361,8 @@ defmodule Stream do
end
@doc """
Maps the given `fun` over `enumerable` and flattens the result.
This function returns a new stream built by appending the result of invoking `fun`
on each element of `enumerable` together.
Creates a stream that will apply the given function on enumeration and
flatten the result, but only one level deep.
## Examples
@@ -429,9 +396,20 @@ defmodule Stream do
lazy enum, fn(f1) -> R.filter(fun, f1) end
end
@doc false
# TODO: Remove on 2.0
# (hard-deprecated in elixir_dispatch)
@doc """
Creates a stream that filters and then maps elements according
to given functions.
Exists for symmetry with `Enum.filter_map/3`.
## Examples
iex> stream = Stream.filter_map(1..6, fn(x) -> rem(x, 2) == 0 end, &(&1 * 2))
iex> Enum.to_list(stream)
[4, 8, 12]
"""
@spec filter_map(Enumerable.t, (element -> as_boolean(term)), (element -> any)) :: Enumerable.t
def filter_map(enum, filter, mapper) do
lazy enum, fn(f1) -> R.filter_map(filter, mapper, f1) end
end
@@ -444,10 +422,6 @@ defmodule Stream do
This operation will block the caller by the given interval
every time a new item is streamed.
Do not use this function to generate a sequence of numbers.
If blocking the caller process is not necessary, use
`Stream.iterate(0, & &1 + 1)` instead.
## Examples
iex> Stream.interval(10) |> Enum.take(10)
@@ -457,7 +431,7 @@ defmodule Stream do
@spec interval(non_neg_integer) :: Enumerable.t
def interval(n) do
unfold 0, fn(count) ->
Process.sleep(n)
:timer.sleep(n)
{count, count + 1}
end
end
@@ -468,7 +442,7 @@ defmodule Stream do
This function is often used with `run/1` since any evaluation
is delayed until the stream is executed. See `run/1` for an example.
"""
@spec into(Enumerable.t, Collectable.t, (term -> term)) :: Enumerable.t
@spec into(Enumerable.t, Collectable.t) :: Enumerable.t
def into(enum, collectable, transform \\ fn x -> x end) do
&do_into(enum, collectable, transform, &1, &2)
end
@@ -516,41 +490,6 @@ defmodule Stream do
lazy enum, fn(f1) -> R.map(fun, f1) end
end
@doc """
Creates a stream that will apply the given function on
every `nth` item from the enumerable.
The first item is always passed to the given function.
`nth` must be a non-negative integer.
## Examples
iex> stream = Stream.map_every(1..10, 2, fn(x) -> x * 2 end)
iex> Enum.to_list(stream)
[2, 2, 6, 4, 10, 6, 14, 8, 18, 10]
iex> stream = Stream.map_every([1, 2, 3, 4, 5], 1, fn(x) -> x * 2 end)
iex> Enum.to_list(stream)
[2, 4, 6, 8, 10]
iex> stream = Stream.map_every(1..5, 0, fn(x) -> x * 2 end)
iex> Enum.to_list(stream)
[1, 2, 3, 4, 5]
"""
@spec map_every(Enumerable.t, non_neg_integer, (element -> any)) :: Enumerable.t
def map_every(enum, nth, fun)
def map_every(enum, 1, fun), do: map(enum, fun)
def map_every(enum, 0, _fun), do: %Stream{enum: enum}
def map_every([], _nth, _fun), do: %Stream{enum: []}
def map_every(enum, nth, fun) when is_integer(nth) and nth > 0 do
lazy enum, nth, fn(f1) -> R.map_every(nth, fun, f1) end
end
@doc """
Creates a stream that will reject elements according to
the given function on enumeration.
@@ -595,8 +534,7 @@ defmodule Stream do
@doc """
Creates a stream that applies the given function to each
element, emits the result and uses the same result as the accumulator
for the next computation. Uses the first element in the enumerable
as the starting value.
for the next computation.
## Examples
@@ -607,7 +545,7 @@ defmodule Stream do
"""
@spec scan(Enumerable.t, (element, acc -> any)) :: Enumerable.t
def scan(enum, fun) do
lazy enum, :first, fn(f1) -> R.scan2(fun, f1) end
lazy enum, :first, fn(f1) -> R.scan_2(fun, f1) end
end
@doc """
@@ -624,7 +562,7 @@ defmodule Stream do
"""
@spec scan(Enumerable.t, acc, (element, acc -> any)) :: Enumerable.t
def scan(enum, acc, fun) do
lazy enum, acc, fn(f1) -> R.scan3(fun, f1) end
lazy enum, acc, fn(f1) -> R.scan_3(fun, f1) end
end
@doc """
@@ -669,7 +607,7 @@ defmodule Stream do
The first item is always included, unless `nth` is 0.
`nth` must be a non-negative integer.
`nth` must be a non-negative integer, or `FunctionClauseError` will be thrown.
## Examples
@@ -687,7 +625,6 @@ defmodule Stream do
"""
@spec take_every(Enumerable.t, non_neg_integer) :: Enumerable.t
def take_every(enum, nth)
def take_every(_enum, 0), do: %Stream{enum: []}
def take_every([], _nth), do: %Stream{enum: []}
@@ -755,10 +692,10 @@ defmodule Stream do
[1, 2, 3]
"""
@spec transform(Enumerable.t, acc, fun) :: Enumerable.t
when fun: (element, acc -> {Enumerable.t, acc} | {:halt, acc}),
acc: any
def transform(enum, acc, reducer) do
@spec transform(Enumerable.t, acc, fun) :: Enumerable.t when
fun: (element, acc -> {Enumerable.t, acc} | {:halt, acc}),
acc: any
def transform(enum, acc, reducer) when is_function(reducer, 2) do
&do_transform(enum, fn -> acc end, reducer, &1, &2, nil)
end
@@ -772,10 +709,11 @@ defmodule Stream do
This function can be seen as a combination of `Stream.resource/3` with
`Stream.transform/3`.
"""
@spec transform(Enumerable.t, (() -> acc), fun, (acc -> term)) :: Enumerable.t
when fun: (element, acc -> {Enumerable.t, acc} | {:halt, acc}),
acc: any
def transform(enum, start_fun, reducer, after_fun) do
@spec transform(Enumerable.t, (() -> acc), fun, (acc -> term)) :: Enumerable.t when
fun: (element, acc -> {Enumerable.t, acc} | {:halt, acc}),
acc: any
def transform(enum, start_fun, reducer, after_fun)
when is_function(start_fun, 0) and is_function(reducer, 2) and is_function(after_fun, 1) do
&do_transform(enum, start_fun, reducer, &1, &2, after_fun)
end
@@ -783,110 +721,92 @@ defmodule Stream do
inner = &do_transform_each(&1, &2, fun)
step = &do_transform_step(&1, &2)
next = &Enumerable.reduce(enumerables, &1, step)
do_transform(user_acc.(), user, fun, :cont, next, inner_acc, inner, after_fun)
do_transform(user_acc.(), user, fun, [], next, inner_acc, inner, after_fun)
end
defp do_transform(user_acc, _user, _fun, _next_op, next, {:halt, inner_acc}, _inner, after_fun) do
next.({:halt, []})
defp do_transform(user_acc, _user, _fun, _next_acc, _next, {:halt, inner_acc}, _inner, after_fun) do
do_after(after_fun, user_acc)
{:halted, inner_acc}
end
defp do_transform(user_acc, user, fun, next_op, next, {:suspend, inner_acc}, inner, after_fun) do
{:suspended, inner_acc, &do_transform(user_acc, user, fun, next_op, next, &1, inner, after_fun)}
defp do_transform(user_acc, user, fun, next_acc, next, {:suspend, inner_acc}, inner, after_fun) do
{:suspended, inner_acc, &do_transform(user_acc, user, fun, next_acc, next, &1, inner, after_fun)}
end
defp do_transform(user_acc, _user, _fun, :halt, _next, {_, inner_acc}, _inner, after_fun) do
do_after(after_fun, user_acc)
{:halted, inner_acc}
end
defp do_transform(user_acc, user, fun, :cont, next, inner_acc, inner, after_fun) do
try do
next.({:cont, []})
catch
kind, reason ->
stacktrace = System.stacktrace
defp do_transform(user_acc, user, fun, next_acc, next, inner_acc, inner, after_fun) do
case next.({:cont, next_acc}) do
{:suspended, [val | next_acc], next} ->
try do
user.(val, user_acc)
catch
kind, reason ->
stacktrace = System.stacktrace
next.({:halt, next_acc})
do_after(after_fun, user_acc)
:erlang.raise(kind, reason, stacktrace)
else
{[], user_acc} ->
do_transform(user_acc, user, fun, next_acc, next, inner_acc, inner, after_fun)
{list, user_acc} when is_list(list) ->
do_list_transform(user_acc, user, fun, next_acc, next, inner_acc, inner,
&Enumerable.List.reduce(list, &1, fun), after_fun)
{:halt, user_acc} ->
next.({:halt, next_acc})
do_after(after_fun, user_acc)
{:halted, elem(inner_acc, 1)}
{other, user_acc} ->
do_enum_transform(user_acc, user, fun, next_acc, next, inner_acc, inner,
&Enumerable.reduce(other, &1, inner), after_fun)
end
{reason, _} ->
do_after(after_fun, user_acc)
:erlang.raise(kind, reason, stacktrace)
else
{:suspended, vals, next} ->
do_transform_user(:lists.reverse(vals), user_acc, user, fun, :cont, next, inner_acc, inner, after_fun)
{_, vals} ->
do_transform_user(:lists.reverse(vals), user_acc, user, fun, :halt, next, inner_acc, inner, after_fun)
{reason, elem(inner_acc, 1)}
end
end
defp do_transform_user([], user_acc, user, fun, next_op, next, inner_acc, inner, after_fun) do
do_transform(user_acc, user, fun, next_op, next, inner_acc, inner, after_fun)
end
defp do_transform_user([val | vals], user_acc, user, fun, next_op, next, inner_acc, inner, after_fun) do
user.(val, user_acc)
catch
kind, reason ->
stacktrace = System.stacktrace
next.({:halt, []})
do_after(after_fun, user_acc)
:erlang.raise(kind, reason, stacktrace)
else
{[], user_acc} ->
do_transform_user(vals, user_acc, user, fun, next_op, next, inner_acc, inner, after_fun)
{list, user_acc} when is_list(list) ->
do_list_transform(vals, user_acc, user, fun, next_op, next, inner_acc, inner,
&Enumerable.List.reduce(list, &1, fun), after_fun)
{:halt, user_acc} ->
next.({:halt, []})
do_after(after_fun, user_acc)
{:halted, elem(inner_acc, 1)}
{other, user_acc} ->
do_enum_transform(vals, user_acc, user, fun, next_op, next, inner_acc, inner,
&Enumerable.reduce(other, &1, inner), after_fun)
end
defp do_list_transform(vals, user_acc, user, fun, next_op, next, inner_acc, inner, reduce, after_fun) do
defp do_list_transform(user_acc, user, fun, next_acc, next, inner_acc, inner, reduce, after_fun) do
try do
reduce.(inner_acc)
catch
kind, reason ->
stacktrace = System.stacktrace
next.({:halt, []})
next.({:halt, next_acc})
do_after(after_fun, user_acc)
:erlang.raise(kind, reason, stacktrace)
else
{:done, acc} ->
do_transform_user(vals, user_acc, user, fun, next_op, next, {:cont, acc}, inner, after_fun)
do_transform(user_acc, user, fun, next_acc, next, {:cont, acc}, inner, after_fun)
{:halted, acc} ->
next.({:halt, []})
next.({:halt, next_acc})
do_after(after_fun, user_acc)
{:halted, acc}
{:suspended, acc, c} ->
{:suspended, acc, &do_list_transform(vals, user_acc, user, fun, next_op, next, &1, inner, c, after_fun)}
{:suspended, acc, &do_list_transform(user_acc, user, fun, next_acc, next, &1, inner, c, after_fun)}
end
end
defp do_enum_transform(vals, user_acc, user, fun, next_op, next, {op, inner_acc}, inner, reduce, after_fun) do
defp do_enum_transform(user_acc, user, fun, next_acc, next, {op, inner_acc}, inner, reduce, after_fun) do
try do
reduce.({op, [:outer | inner_acc]})
catch
kind, reason ->
stacktrace = System.stacktrace
next.({:halt, []})
next.({:halt, next_acc})
do_after(after_fun, user_acc)
:erlang.raise(kind, reason, stacktrace)
else
# Only take into account outer halts when the op is not halt itself.
# Otherwise, we were the ones wishing to halt, so we should just stop.
{:halted, [:outer | acc]} when op != :halt ->
do_transform_user(vals, user_acc, user, fun, next_op, next, {:cont, acc}, inner, after_fun)
do_transform(user_acc, user, fun, next_acc, next, {:cont, acc}, inner, after_fun)
{:halted, [_ | acc]} ->
next.({:halt, []})
next.({:halt, next_acc})
do_after(after_fun, user_acc)
{:halted, acc}
{:done, [_ | acc]} ->
do_transform_user(vals, user_acc, user, fun, next_op, next, {:cont, acc}, inner, after_fun)
do_transform(user_acc, user, fun, next_acc, next, {:cont, acc}, inner, after_fun)
{:suspended, [_ | acc], c} ->
{:suspended, acc, &do_enum_transform(vals, user_acc, user, fun, next_op, next, &1, inner, c, after_fun)}
{:suspended, acc, &do_enum_transform(user_acc, user, fun, next_acc, next, &1, inner, c, after_fun)}
end
end
@@ -917,51 +837,20 @@ defmodule Stream do
iex> Stream.uniq([1, 2, 3, 3, 2, 1]) |> Enum.to_list
[1, 2, 3]
"""
@spec uniq(Enumerable.t) :: Enumerable.t
def uniq(enum) do
uniq_by(enum, fn x -> x end)
end
@doc false
# TODO: Remove on 2.0
# (hard-deprecated in elixir_dispatch)
def uniq(enum, fun) do
uniq_by(enum, fun)
end
@doc """
Creates a stream that only emits elements if they are unique, by removing the
elements for which function `fun` returned duplicate items.
The function `fun` maps every element to a term which is used to
determine if two elements are duplicates.
Keep in mind that, in order to know if an element is unique
or not, this function needs to store all unique values emitted
by the stream. Therefore, if the stream is infinite, the number
of items stored will grow infinitely, never being garbage collected.
## Example
iex> Stream.uniq_by([{1, :x}, {2, :y}, {1, :z}], fn {x, _} -> x end) |> Enum.to_list
iex> Stream.uniq([{1, :x}, {2, :y}, {2, :z}, {1, :x}], fn {x, _} -> x end) |> Enum.to_list
[{1, :x}, {2, :y}]
iex> Stream.uniq_by([a: {:tea, 2}, b: {:tea, 2}, c: {:coffee, 1}], fn {_, y} -> y end) |> Enum.to_list
[a: {:tea, 2}, c: {:coffee, 1}]
"""
@spec uniq_by(Enumerable.t, (element -> term)) :: Enumerable.t
def uniq_by(enum, fun) do
lazy enum, %{}, fn f1 -> R.uniq_by(fun, f1) end
@spec uniq(Enumerable.t) :: Enumerable.t
@spec uniq(Enumerable.t, (element -> term)) :: Enumerable.t
def uniq(enum, fun \\ fn x -> x end) do
lazy enum, %{}, fn f1 -> R.uniq(fun, f1) end
end
@doc """
Creates a stream where each item in the enumerable will
be wrapped in a tuple alongside its index.
If an `offset` is given, we will index from the given offset instead of from zero.
## Examples
iex> stream = Stream.with_index([1, 2, 3])
@@ -973,6 +862,7 @@ defmodule Stream do
[{1, 3}, {2, 4}, {3, 5}]
"""
@spec with_index(Enumerable.t) :: Enumerable.t
@spec with_index(Enumerable.t, integer) :: Enumerable.t
def with_index(enum, offset \\ 0) do
lazy enum, offset, fn(f1) -> R.with_index(f1) end
@@ -1030,35 +920,15 @@ defmodule Stream do
"""
@spec zip(Enumerable.t, Enumerable.t) :: Enumerable.t
def zip(left, right), do: zip([left, right])
@doc """
Zips corresponding elements from a collection of enumerables
into one stream of tuples.
The zipping finishes as soon as any enumerable completes.
## Examples
iex> concat = Stream.concat(1..3, 4..6)
iex> cycle = Stream.cycle(["foo", "bar", "baz"])
iex> Stream.zip([concat, [:a, :b, :c], cycle]) |> Enum.to_list
[{1, :a, "foo"}, {2, :b, "bar"}, {3, :c, "baz"}]
"""
@spec zip([Enumerable.t]) :: Enumerable.t
def zip(enumerables) do
def zip(left, right) do
step = &do_zip_step(&1, &2)
enum_funs = Enum.map(enumerables, fn enum ->
{&Enumerable.reduce(enum, &1, step), :cont}
end)
left_fun = &Enumerable.reduce(left, &1, step)
right_fun = &Enumerable.reduce(right, &1, step)
&do_zip(enum_funs, &1, &2)
# Return a function as a lazy enumerator.
&do_zip([{left_fun, []}, {right_fun, []}], &1, &2)
end
# This implementation of do_zip/3 works for any number of
# streams to zip, even if right now zip/2 only zips two streams.
defp do_zip(zips, {:halt, acc}, _fun) do
do_zip_close(zips)
{:halted, acc}
@@ -1070,7 +940,7 @@ defmodule Stream do
defp do_zip(zips, {:cont, acc}, callback) do
try do
do_zip_next_tuple(zips, acc, callback, [], [])
do_zip(zips, acc, callback, [], [])
catch
kind, reason ->
stacktrace = System.stacktrace
@@ -1079,47 +949,34 @@ defmodule Stream do
else
{:next, buffer, acc} ->
do_zip(buffer, acc, callback)
{:done, _acc} = other ->
other
{:done, _} = o ->
o
end
end
# do_zip_next_tuple/5 computes the next tuple formed by
# the next element of each zipped stream.
defp do_zip_next_tuple([{_, :halt} | zips], acc, _callback, _yielded_elems, buffer) do
do_zip_close(:lists.reverse(buffer, zips))
{:done, acc}
end
defp do_zip_next_tuple([{fun, :cont} | zips], acc, callback, yielded_elems, buffer) do
case fun.({:cont, []}) do
{:suspended, [elem], fun} ->
do_zip_next_tuple(zips, acc, callback, [elem | yielded_elems], [{fun, :cont} | buffer])
{_, [elem]} ->
do_zip_next_tuple(zips, acc, callback, [elem | yielded_elems], [{fun, :halt} | buffer])
{_, []} ->
# The current zipped stream terminated, so we close all the streams
# and return {:halted, acc} (which is returned as is by do_zip/3).
do_zip_close(:lists.reverse(buffer, zips))
defp do_zip([{fun, fun_acc} | t], acc, callback, list, buffer) do
case fun.({:cont, fun_acc}) do
{:suspended, [i | fun_acc], fun} ->
do_zip(t, acc, callback, [i | list], [{fun, fun_acc} | buffer])
{_, _} ->
do_zip_close(:lists.reverse(buffer, t))
{:done, acc}
end
end
defp do_zip_next_tuple([] = _zips, acc, callback, yielded_elems, buffer) do
# "yielded_elems" is a reversed list of results for the current iteration of
# zipping: it needs to be reversed and converted to a tuple to have the next
# tuple in the list resulting from zipping.
zipped = List.to_tuple(:lists.reverse(yielded_elems))
defp do_zip([], acc, callback, list, buffer) do
zipped = List.to_tuple(:lists.reverse(list))
{:next, :lists.reverse(buffer), callback.(zipped, acc)}
end
defp do_zip_close(zips) do
:lists.foreach(fn {fun, _} -> fun.({:halt, []}) end, zips)
defp do_zip_close([]), do: :ok
defp do_zip_close([{fun, acc} | t]) do
fun.({:halt, acc})
do_zip_close(t)
end
defp do_zip_step(x, []) do
{:suspend, [x]}
defp do_zip_step(x, acc) do
{:suspend, [x | acc]}
end
## Sources
@@ -1138,10 +995,6 @@ defmodule Stream do
@spec cycle(Enumerable.t) :: Enumerable.t
def cycle(enumerable)
def cycle([]) do
raise ArgumentError, "cannot cycle over empty enumerable"
end
def cycle(enumerable) when is_list(enumerable) do
unfold {enumerable, enumerable}, fn
{source, [h | t]} -> {h, {source, t}}
@@ -1172,8 +1025,6 @@ defmodule Stream do
{:stream_cycle, acc} ->
{:halted, acc}
else
{state, []} when state in [:done, :halted] ->
raise ArgumentError, "cannot cycle over empty enumerable"
{state, acc} when state in [:done, :halted] ->
do_cycle(cycle, cycle, {:cont, acc})
{:suspended, acc, continuation} ->
@@ -1221,7 +1072,7 @@ defmodule Stream do
"""
@spec repeatedly((() -> element)) :: Enumerable.t
def repeatedly(generator_fun) do
def repeatedly(generator_fun) when is_function(generator_fun, 0) do
&do_repeatedly(generator_fun, &1, &2)
end
@@ -1390,12 +1241,12 @@ defmodule Stream do
@compile {:inline, lazy: 2, lazy: 3, lazy: 4}
defp lazy(%Stream{done: nil, funs: funs} = lazy, fun),
do: %{lazy | funs: [fun | funs]}
do: %{lazy | funs: [fun | funs] }
defp lazy(enum, fun),
do: %Stream{enum: enum, funs: [fun]}
defp lazy(%Stream{done: nil, funs: funs, accs: accs} = lazy, acc, fun),
do: %{lazy | funs: [fun | funs], accs: [acc | accs]}
do: %{lazy | funs: [fun | funs], accs: [acc | accs] }
defp lazy(enum, acc, fun),
do: %Stream{enum: enum, funs: [fun], accs: [acc]}
+98 -121
View File
@@ -1,113 +1,98 @@
defmodule Stream.Reducers do
# Collection of reducers and utilities shared by Enum and Stream.
# Collection of reducers shared by Enum and Stream.
@moduledoc false
def chunk_every(chunk_by, enumerable, count, step, leftover) do
limit = :erlang.max(count, step)
chunk_by.(enumerable, {[], 0}, fn entry, {acc_buffer, acc_count} ->
acc_buffer = [entry | acc_buffer]
acc_count = acc_count + 1
new_state =
if acc_count >= limit do
remaining = acc_count - step
{Enum.take(acc_buffer, remaining), remaining}
else
{acc_buffer, acc_count}
end
if acc_count == count do
{:cont, :lists.reverse(acc_buffer), new_state}
else
{:cont, new_state}
end
end, fn {acc_buffer, acc_count} ->
if leftover == :discard or acc_count == 0 do
{:cont, []}
else
{:cont, :lists.reverse(acc_buffer, Enum.take(leftover, count - acc_count)), []}
end
end)
end
def chunk_by(chunk_by, enumerable, fun) do
chunk_by.(enumerable, nil, fn
entry, nil ->
{:cont, {[entry], fun.(entry)}}
entry, {acc, value} ->
case fun.(entry) do
^value -> {:cont, {[entry | acc], value}}
new_value -> {:cont, :lists.reverse(acc), {[entry], new_value}}
end
end, fn
nil -> {:cont, :done}
{acc, _value} -> {:cont, :lists.reverse(acc), :done}
end)
end
defmacro chunk_while(callback, fun \\ nil) do
defmacro chunk(n, step, limit, f \\ nil) do
quote do
fn entry, acc(head, acc, tail) ->
case unquote(callback).(entry, acc) do
{:cont, emit, acc} -> next_with_acc(unquote(fun), emit, head, acc, tail)
{:cont, acc} -> skip(acc(head, acc, tail))
{:halt, acc} -> {:halt, acc(head, acc, tail)}
fn entry, acc(h, {buffer, count}, t) ->
buffer = [entry | buffer]
count = count + 1
new =
if count >= unquote(limit) do
left = count - unquote(step)
{Enum.take(buffer, left), left}
else
{buffer, count}
end
if count == unquote(n) do
next_with_acc(unquote(f), :lists.reverse(buffer), h, new, t)
else
skip(acc(h, new, t))
end
end
end
end
defmacro dedup(callback, fun \\ nil) do
defmacro chunk_by(callback, f \\ nil) do
quote do
fn entry, acc(head, prev, tail) = acc ->
fn
entry, acc(h, {buffer, value}, t) ->
new_value = unquote(callback).(entry)
if new_value == value do
skip(acc(h, {[entry | buffer], value}, t))
else
next_with_acc(unquote(f), :lists.reverse(buffer), h, {[entry], new_value}, t)
end
entry, acc(h, nil, t) ->
skip(acc(h, {[entry], unquote(callback).(entry)}, t))
end
end
end
defmacro dedup(callback, f \\ nil) do
quote do
fn(entry, acc(h, prev, t) = acc) ->
value = unquote(callback).(entry)
case prev do
{:value, ^value} -> skip(acc)
_ -> next_with_acc(unquote(fun), entry, head, {:value, value}, tail)
{:value, ^value} -> skip(acc)
_ -> next_with_acc(unquote(f), entry, h, {:value, value}, t)
end
end
end
end
defmacro drop(fun \\ nil) do
defmacro drop(f \\ nil) do
quote do
fn
_entry, acc(head, amount, tail) when amount > 0 ->
skip(acc(head, amount - 1, tail))
entry, acc(head, amount, tail) ->
next_with_acc(unquote(fun), entry, head, amount, tail)
_entry, acc(h, n, t) when n > 0 ->
skip(acc(h, n-1, t))
entry, acc(h, n, t) ->
next_with_acc(unquote(f), entry, h, n, t)
end
end
end
defmacro drop_every(nth, fun \\ nil) do
defmacro drop_every(nth, f \\ nil) do
quote do
fn
entry, acc(head, curr, tail) when curr in [unquote(nth), :first] ->
skip(acc(head, 1, tail))
entry, acc(head, curr, tail) ->
next_with_acc(unquote(fun), entry, head, curr + 1, tail)
entry, acc(h, n, t) when n === :first
when n === unquote(nth) ->
skip(acc(h, 1, t))
entry, acc(h, n, t) ->
next_with_acc(unquote(f), entry, h, n+1, t)
end
end
end
defmacro drop_while(callback, fun \\ nil) do
defmacro drop_while(callback, f \\ nil) do
quote do
fn entry, acc(head, bool, tail) = original ->
fn entry, acc(h, bool, t) = orig ->
if bool and unquote(callback).(entry) do
skip(original)
skip(orig)
else
next_with_acc(unquote(fun), entry, head, false, tail)
next_with_acc(unquote(f), entry, h, false, t)
end
end
end
end
defmacro filter(callback, fun \\ nil) do
defmacro filter(callback, f \\ nil) do
quote do
fn entry, acc ->
fn(entry, acc) ->
if unquote(callback).(entry) do
next(unquote(fun), entry, acc)
next(unquote(f), entry, acc)
else
skip(acc)
end
@@ -115,11 +100,11 @@ defmodule Stream.Reducers do
end
end
defmacro filter_map(filter, mapper, fun \\ nil) do
defmacro filter_map(filter, mapper, f \\ nil) do
quote do
fn entry, acc ->
fn(entry, acc) ->
if unquote(filter).(entry) do
next(unquote(fun), unquote(mapper).(entry), acc)
next(unquote(f), unquote(mapper).(entry), acc)
else
skip(acc)
end
@@ -127,30 +112,19 @@ defmodule Stream.Reducers do
end
end
defmacro map(callback, fun \\ nil) do
defmacro map(callback, f \\ nil) do
quote do
fn entry, acc ->
next(unquote(fun), unquote(callback).(entry), acc)
fn(entry, acc) ->
next(unquote(f), unquote(callback).(entry), acc)
end
end
end
defmacro map_every(nth, mapper, fun \\ nil) do
defmacro reject(callback, f \\ nil) do
quote do
fn
entry, acc(head, curr, tail) when curr in [unquote(nth), :first] ->
next_with_acc(unquote(fun), unquote(mapper).(entry), head, 1, tail)
entry, acc(head, curr, tail) ->
next_with_acc(unquote(fun), entry, head, curr + 1, tail)
end
end
end
defmacro reject(callback, fun \\ nil) do
quote do
fn entry, acc ->
fn(entry, acc) ->
unless unquote(callback).(entry) do
next(unquote(fun), entry, acc)
next(unquote(f), entry, acc)
else
skip(acc)
end
@@ -158,59 +132,62 @@ defmodule Stream.Reducers do
end
end
defmacro scan2(callback, fun \\ nil) do
defmacro scan_2(callback, f \\ nil) do
quote do
fn
entry, acc(head, :first, tail) ->
next_with_acc(unquote(fun), entry, head, {:ok, entry}, tail)
entry, acc(head, {:ok, acc}, tail) ->
entry, acc(h, :first, t) ->
next_with_acc(unquote(f), entry, h, {:ok, entry}, t)
entry, acc(h, {:ok, acc}, t) ->
value = unquote(callback).(entry, acc)
next_with_acc(unquote(fun), value, head, {:ok, value}, tail)
next_with_acc(unquote(f), value, h, {:ok, value}, t)
end
end
end
defmacro scan3(callback, fun \\ nil) do
defmacro scan_3(callback, f \\ nil) do
quote do
fn entry, acc(head, acc, tail) ->
fn(entry, acc(h, acc, t)) ->
value = unquote(callback).(entry, acc)
next_with_acc(unquote(fun), value, head, value, tail)
next_with_acc(unquote(f), value, h, value, t)
end
end
end
defmacro take(fun \\ nil) do
defmacro take(f \\ nil) do
quote do
fn entry, acc(head, curr, tail) = original ->
case curr do
fn(entry, acc(h, n, t) = orig) ->
case n do
0 ->
{:halt, original}
{:halt, orig}
1 ->
{_, acc} = next_with_acc(unquote(fun), entry, head, 0, tail)
{:halt, acc}
case next_with_acc(unquote(f), entry, h, n-1, t) do
{:cont, acc} -> {:halt, acc}
reason -> reason
end
_ ->
next_with_acc(unquote(fun), entry, head, curr - 1, tail)
next_with_acc(unquote(f), entry, h, n-1, t)
end
end
end
end
defmacro take_every(nth, fun \\ nil) do
defmacro take_every(nth, f \\ nil) do
quote do
fn
entry, acc(head, curr, tail) when curr in [unquote(nth), :first] ->
next_with_acc(unquote(fun), entry, head, 1, tail)
entry, acc(head, curr, tail) ->
skip(acc(head, curr + 1, tail))
entry, acc(h, n, t) when n === :first
when n === unquote(nth) ->
next_with_acc(unquote(f), entry, h, 1, t)
entry, acc(h, n, t) ->
skip(acc(h, n+1, t))
end
end
end
defmacro take_while(callback, fun \\ nil) do
defmacro take_while(callback, f \\ nil) do
quote do
fn entry, acc ->
fn(entry, acc) ->
if unquote(callback).(entry) do
next(unquote(fun), entry, acc)
next(unquote(f), entry, acc)
else
{:halt, acc}
end
@@ -218,23 +195,23 @@ defmodule Stream.Reducers do
end
end
defmacro uniq_by(callback, fun \\ nil) do
defmacro uniq(callback, f \\ nil) do
quote do
fn entry, acc(head, prev, tail) = original ->
fn(entry, acc(h, prev, t) = acc) ->
value = unquote(callback).(entry)
if Map.has_key?(prev, value) do
skip(original)
skip(acc)
else
next_with_acc(unquote(fun), entry, head, Map.put(prev, value, true), tail)
next_with_acc(unquote(f), entry, h, Map.put(prev, value, true), t)
end
end
end
end
defmacro with_index(fun \\ nil) do
defmacro with_index(f \\ nil) do
quote do
fn entry, acc(head, counter, tail) ->
next_with_acc(unquote(fun), {entry, counter}, head, counter + 1, tail)
fn(entry, acc(h, counter, t)) ->
next_with_acc(unquote(f), {entry, counter}, h, counter + 1, t)
end
end
end
+202 -225
View File
@@ -4,10 +4,10 @@ defmodule String do
@moduledoc ~S"""
A String in Elixir is a UTF-8 encoded binary.
## Codepoints and grapheme cluster
## Codepoints and graphemes
The functions in this module act according to the Unicode
Standard, version 9.0.0.
Standard, version 6.3.0.
As per the standard, a codepoint is a single Unicode Character,
which may be represented by one or more bytes.
@@ -22,11 +22,12 @@ defmodule String do
iex> String.length("é")
1
Furthermore, this module also presents the concept of grapheme cluster
(from now on referenced as graphemes). Graphemes can consist of multiple
codepoints that may be perceived as a single character by readers. For
example, "é" can be represented either as a single "e with acute" codepoint
or as the letter "e" followed by a "combining acute accent" (two codepoints):
Furthermore, this module also presents the concept of
graphemes. A single grapheme can consist of multiple codepoints
that may be perceived as a single character by readers. For example,
the "é" grapheme can be represented either as a single "e with acute"
codepoint (like above), or as the letter "e" followed by a
"combining acute accent" (two codepoints):
iex> string = "\u0065\u0301"
iex> byte_size(string)
@@ -43,9 +44,9 @@ defmodule String do
Graphemes can also be two characters that are interpreted
as one by some languages. For example, some languages may
consider "ch" as a single character. However, since this
information depends on the locale, it is not taken into account
by this module.
consider "ch" as a grapheme. However, since this information
depends on the locale, it is not taken into account by this
module.
In general, the functions in this module rely on the Unicode
Standard, but do not contain any of the locale specific behaviour.
@@ -55,9 +56,6 @@ defmodule String do
The current Elixir version implements Extended Grapheme Cluster
algorithm.
For converting a binary to a different encoding and for Unicode
normalization mechanisms, see Erlang's `:unicode` module.
## String and binary operations
To act according to the Unicode Standard, many functions
@@ -108,8 +106,8 @@ defmodule String do
While this is much better (we don't traverse `full` twice),
it could still be improved. In this case, since we want to
extract a substring from a string, we can use `Kernel.byte_size/1`
and `Kernel.binary_part/3` as there is no chance we will slice in
extract a substring from a string, we can use `byte_size/1`
and `binary_part/3` as there is no chance we will slice in
the middle of a codepoint made of more than one byte:
iex> take_prefix = fn full, prefix ->
@@ -175,15 +173,14 @@ defmodule String do
a correct result even if an invalid codepoint is fed into it.
In other words, this module expects invalid data to be detected
elsewhere, usually when retrieving data from the external source.
For example, a driver that reads strings from a database will be
responsible to check the validity of the encoding. `String.chunk/2`
can be used for breaking a string into valid and invalid parts.
when retrieving data from the external source. For example, a
driver that reads strings from a database will be
responsible to check the validity of the encoding.
## Patterns
Many functions in this module work with patterns. For example,
`String.split/2` can split a string into multiple patterns given
String.split/2 can split a string into multiple patterns given
a pattern. This pattern can be a string, a list of strings or
a compiled pattern:
@@ -211,49 +208,35 @@ defmodule String do
@doc """
Checks if a string contains only printable characters.
Takes an optional `limit` as a second argument. `printable?/2` only
checks the printability of the string up to the `limit`.
## Examples
iex> String.printable?("abc")
true
iex> String.printable?("abc" <> <<0>>)
false
iex> String.printable?("abc" <> <<0>>, 2)
true
"""
@spec printable?(t) :: boolean
@spec printable?(t, non_neg_integer | :infinity) :: boolean
def printable?(string, counter \\ :infinity)
def printable?(string)
def printable?(<<>>, _), do: true
def printable?(_, 0), do: true
for char <- 0x20..0x7E do
def printable?(<<unquote(char), rest::binary>>, counter) do
printable?(rest, decrement(counter))
end
end
for char <- '\n\r\t\v\b\f\e\d\a' do
def printable?(<<unquote(char), rest::binary>>, counter) do
printable?(rest, decrement(counter))
end
end
def printable?(<<char::utf8, rest::binary>>, counter)
when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF do
printable?(rest, decrement(counter))
def printable?(<<h::utf8, t::binary>>)
when h in 0x20..0x7E
when h in 0xA0..0xD7FF
when h in 0xE000..0xFFFD
when h in 0x10000..0x10FFFF do
printable?(t)
end
def printable?(binary, _) when is_binary(binary), do: false
def printable?(<<?\n, t::binary>>), do: printable?(t)
def printable?(<<?\r, t::binary>>), do: printable?(t)
def printable?(<<?\t, t::binary>>), do: printable?(t)
def printable?(<<?\v, t::binary>>), do: printable?(t)
def printable?(<<?\b, t::binary>>), do: printable?(t)
def printable?(<<?\f, t::binary>>), do: printable?(t)
def printable?(<<?\e, t::binary>>), do: printable?(t)
def printable?(<<?\d, t::binary>>), do: printable?(t)
def printable?(<<?\a, t::binary>>), do: printable?(t)
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
def printable?(<<>>), do: true
def printable?(binary) when is_binary(binary), do: false
@doc ~S"""
Divides a string into substrings at each Unicode whitespace
@@ -283,29 +266,15 @@ defmodule String do
Divides a string into substrings based on a pattern.
Returns a list of these substrings. The pattern can
be a string, a list of strings, or a regular expression.
be a string, a list of strings or a regular expression.
The string is split into as many parts as possible by
default, but can be controlled via the `:parts` option.
default, but can be controlled via the `parts: pos_integer` option.
If you pass `parts: :infinity`, it will return all possible parts
(`:infinity` is the default).
Empty strings are only removed from the result if the
`:trim` option is set to `true`.
When the pattern used is a regular expression, the string is
split using `Regex.split/3`.
## Options
* `:parts` (positive integer or `:infinity`) - the string
is split into at most as many parts as this options specifies.
If `:infinity`, the string will be split into all possible
parts. Defaults to `:infinity`.
* `:trim` (boolean) - if `true`, empty strings are removed from
the resulting list.
This function also accepts all options accepted by `Regex.split/3`
if `pattern` is a regular expression.
`trim` option is set to `true` (default is `false`).
## Examples
@@ -336,9 +305,6 @@ defmodule String do
iex> String.split(" a b c ", ~r{\s}, trim: true)
["a", "b", "c"]
iex> String.split("abc", ~r{b}, include_captures: true)
["a", "b", "c"]
Splitting on empty patterns returns graphemes:
iex> String.split("abc", ~r{})
@@ -360,7 +326,8 @@ defmodule String do
["1", "2", "3", "4"]
"""
@spec split(t, pattern | Regex.t, keyword) :: [t]
@spec split(t, pattern | Regex.t) :: [t]
@spec split(t, pattern | Regex.t, Keyword.t) :: [t]
def split(string, pattern, options \\ [])
def split(string, %Regex{} = pattern, options) when is_binary(string) do
@@ -371,6 +338,8 @@ defmodule String do
:binary.split(string, pattern, [:global])
end
# TODO: Use :trim_all with :binary.split/3 whenever parts is
# infinity and pattern != "" and at least Erlang 18.2 is required.
def split(string, pattern, options) when is_binary(string) do
parts = Keyword.get(options, :parts, :infinity)
trim = Keyword.get(options, :trim, false)
@@ -404,20 +373,8 @@ defmodule String do
## Options
* :trim - when `true`, does not emit empty patterns
## Examples
iex> String.splitter("1,2 3,4 5,6 7,8,...,99999", [" ", ","]) |> Enum.take(4)
["1", "2", "3", "4"]
iex> String.splitter("abcd", "") |> Enum.take(10)
["a", "b", "c", "d", ""]
iex> String.splitter("abcd", "", trim: true) |> Enum.take(10)
["a", "b", "c", "d"]
"""
@spec splitter(t, pattern, keyword) :: Enumerable.t
@spec splitter(t, pattern, Keyword.t) :: Enumerable.t
def splitter(string, pattern, options \\ []) do
pattern = maybe_compile_pattern(pattern)
trim = Keyword.get(options, :trim, false)
@@ -433,10 +390,15 @@ defmodule String do
end
defp do_splitter(bin, pattern, trim) do
case :binary.split(bin, pattern) do
["", second] when trim -> do_splitter(second, pattern, trim)
[first, second] -> {first, second}
[first] -> {first, :nomatch}
case :binary.match(bin, pattern) do
{0, length} when trim ->
do_splitter(:binary.part(bin, length, byte_size(bin) - length), pattern, trim)
{pos, length} ->
final = pos + length
{:binary.part(bin, 0, pos),
:binary.part(bin, final, byte_size(bin) - final)}
:nomatch ->
{bin, :nomatch}
end
end
@@ -482,7 +444,7 @@ defmodule String do
end
def split_at(string, position) when is_integer(position) and position < 0 do
position = length(string) + position
position = length(string) - abs(position)
case position >= 0 do
true -> do_split_at(string, position)
false -> {"", string}
@@ -618,13 +580,11 @@ defmodule String do
end
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by 1.5
defdelegate rstrip(binary), to: String.Break, as: :trim_trailing
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by 1.5
def rstrip(string, char) when is_integer(char) do
replace_trailing(string, <<char::utf8>>, "")
end
@@ -634,11 +594,6 @@ defmodule String do
Returns the string untouched if there are no occurrences.
If `match` is `""`, this function raises an `ArgumentError` exception: this
happens because this function replaces **all** the occurrences of `match` at
the beginning of `string`, and it's impossible to replace "multiple"
occurrences of `""`.
## Examples
iex> String.replace_leading("hello world", "hello ", "")
@@ -652,13 +607,8 @@ defmodule String do
"ola ola world"
"""
@spec replace_leading(t, t, t) :: t | no_return
def replace_leading(string, match, replacement)
when is_binary(string) and is_binary(match) and is_binary(replacement) do
if match == "" do
raise ArgumentError, "cannot use an empty string as the match to replace"
end
prefix_size = byte_size(match)
suffix_size = byte_size(string) - prefix_size
replace_leading(string, match, replacement, prefix_size, suffix_size, 0)
@@ -666,15 +616,15 @@ defmodule String do
defp replace_leading(string, match, replacement, prefix_size, suffix_size, acc) when suffix_size >= 0 do
case string do
<<prefix::size(prefix_size)-binary, suffix::binary>> when prefix == match ->
<<prefix::size(prefix_size)-binary, suffix::size(suffix_size)-binary>> when prefix == match ->
replace_leading(suffix, match, replacement, prefix_size, suffix_size - prefix_size, acc + 1)
_ ->
prepend_unless_empty(duplicate(replacement, acc), string)
duplicate(replacement, acc) <> string
end
end
defp replace_leading(string, _match, replacement, _prefix_size, _suffix_size, acc) do
prepend_unless_empty(duplicate(replacement, acc), string)
duplicate(replacement, acc) <> string
end
@doc """
@@ -682,11 +632,6 @@ defmodule String do
Returns the string untouched if there are no occurrences.
If `match` is `""`, this function raises an `ArgumentError` exception: this
happens because this function replaces **all** the occurrences of `match` at
the end of `string`, and it's impossible to replace "multiple" occurrences of
`""`.
## Examples
iex> String.replace_trailing("hello world", " world", "")
@@ -700,13 +645,8 @@ defmodule String do
"hello mundo mundo"
"""
@spec replace_trailing(t, t, t) :: t | no_return
def replace_trailing(string, match, replacement)
when is_binary(string) and is_binary(match) and is_binary(replacement) do
if match == "" do
raise ArgumentError, "cannot use an empty string as the match to replace"
end
suffix_size = byte_size(match)
prefix_size = byte_size(string) - suffix_size
replace_trailing(string, match, replacement, prefix_size, suffix_size, 0)
@@ -714,22 +654,21 @@ defmodule String do
defp replace_trailing(string, match, replacement, prefix_size, suffix_size, acc) when prefix_size >= 0 do
case string do
<<prefix::size(prefix_size)-binary, suffix::binary>> when suffix == match ->
<<prefix::size(prefix_size)-binary, suffix::size(suffix_size)-binary>> when suffix == match ->
replace_trailing(prefix, match, replacement, prefix_size - suffix_size, suffix_size, acc + 1)
_ ->
append_unless_empty(string, duplicate(replacement, acc))
string <> duplicate(replacement, acc)
end
end
defp replace_trailing(string, _match, replacement, _prefix_size, _suffix_size, acc) do
append_unless_empty(string, duplicate(replacement, acc))
string <> duplicate(replacement, acc)
end
@doc """
Replaces prefix in `string` by `replacement` if it matches `match`.
Returns the string untouched if there is no match. If `match` is an empty
string (`""`), `replacement` is just prepended to `string`.
Returns the string untouched if there is no match.
## Examples
@@ -747,18 +686,15 @@ defmodule String do
iex> String.replace_prefix("hello hello world", "hello ", "ola ")
"ola hello world"
iex> String.replace_prefix("world", "", "hello ")
"hello world"
"""
@spec replace_prefix(t, t, t) :: t
def replace_prefix(string, match, replacement)
when is_binary(string) and is_binary(match) and is_binary(replacement) do
prefix_size = byte_size(match)
suffix_size = byte_size(string) - prefix_size
case string do
<<prefix::size(prefix_size)-binary, suffix::binary>> when prefix == match ->
prepend_unless_empty(replacement, suffix)
<<prefix::size(prefix_size)-binary, suffix::size(suffix_size)-binary>> when prefix == match ->
replacement <> suffix
_ ->
string
end
@@ -767,8 +703,7 @@ defmodule String do
@doc """
Replaces suffix in `string` by `replacement` if it matches `match`.
Returns the string untouched if there is no match. If `match` is an empty
string (`""`), `replacement` is just appended to `string`.
Returns the string untouched if there is no match.
## Examples
@@ -786,54 +721,38 @@ defmodule String do
iex> String.replace_suffix("hello world world", " world", " mundo")
"hello world mundo"
iex> String.replace_suffix("hello", "", " world")
"hello world"
"""
@spec replace_suffix(t, t, t) :: t
def replace_suffix(string, match, replacement)
when is_binary(string) and is_binary(match) and is_binary(replacement) do
suffix_size = byte_size(match)
prefix_size = byte_size(string) - suffix_size
case string do
<<prefix::size(prefix_size)-binary, suffix::binary>> when suffix == match ->
append_unless_empty(prefix, replacement)
<<prefix::size(prefix_size)-binary, suffix::size(suffix_size)-binary>> when suffix == match ->
prefix <> replacement
_ ->
string
end
end
@compile {:inline, prepend_unless_empty: 2, append_unless_empty: 2}
defp prepend_unless_empty("", suffix), do: suffix
defp prepend_unless_empty(prefix, suffix), do: prefix <> suffix
defp append_unless_empty(prefix, ""), do: prefix
defp append_unless_empty(prefix, suffix), do: prefix <> suffix
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by 1.5
defdelegate lstrip(binary), to: String.Break, as: :trim_leading
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by 1.5
def lstrip(string, char) when is_integer(char) do
replace_leading(string, <<char::utf8>>, "")
end
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by 1.5
def strip(string) do
trim(string)
end
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by 1.5
def strip(string, char) do
trim(string, <<char::utf8>>)
end
@@ -1053,15 +972,13 @@ defmodule String do
end
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by 1.5
def rjust(subject, len, pad \\ ?\s) when is_integer(pad) and is_integer(len) and len >= 0 do
pad(:leading, subject, len, [<<pad::utf8>>])
end
@doc false
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
# TODO: Deprecate by 1.5
def ljust(subject, len, pad \\ ?\s) when is_integer(pad) and is_integer(len) and len >= 0 do
pad(:trailing, subject, len, [<<pad::utf8>>])
end
@@ -1070,23 +987,11 @@ defmodule String do
Returns a new string created by replacing occurrences of `pattern` in
`subject` with `replacement`.
By default, it replaces all occurrences, unless the `global` option is
set to `false`, where it will only replace the first one
The `pattern` may be a string or a regular expression.
By default it replaces all occurrences but this behaviour can be controlled
through the `:global` option; see the "Options" section below.
## Options
* `:global` - (boolean) if `true`, all occurrences of `pattern` are replaced
with `replacement`, otherwise only the first occurrence is
replaced. Defaults to `true`
* `:insert_replaced` - (integer or list of integers) specifies the position
where to insert the replaced part inside the `replacement`. If any
position given in the `:insert_replaced` option is larger than the
replacement string, or is negative, an `ArgumentError` is raised. See the
examples below
## Examples
iex> String.replace("a,b,c", ",", "-")
@@ -1102,13 +1007,11 @@ defmodule String do
iex> String.replace("a,b,c", ~r/,(.)/, ",\\1\\g{1}")
"a,bb,cc"
Notice we had to escape the backslash escape character (i.e., we used `\\N`
instead of just `\N` to escape the backslash; same thing for `\\g{N}`). By
giving `\0`, one can inject the whole matched pattern in the replacement
string.
Notice we had to escape the escape character `\`. By giving `\0`,
one can inject the whole matched pattern in the replacement string.
When the pattern is a string, a developer can use the replaced part inside
the `replacement` by using the `:insert_replaced` option and specifying the
the `replacement` by using the `:insert_replace` option and specifying the
position(s) inside the `replacement` where the string pattern will be
inserted:
@@ -1121,8 +1024,10 @@ defmodule String do
iex> String.replace("a,b,c", ",", "[]", insert_replaced: [1, 1])
"a[,,]b[,,]c"
If any position given in the `:insert_replace` option is larger than the
replacement string, or is negative, an `ArgumentError` is raised.
"""
@spec replace(t, pattern | Regex.t, t, keyword) :: t
@spec replace(t, pattern | Regex.t, t, Keyword.t) :: t
def replace(subject, pattern, replacement, options \\ []) when is_binary(replacement) do
if Regex.regex?(pattern) do
Regex.replace(pattern, subject, replacement, global: options[:global])
@@ -1187,13 +1092,9 @@ defmodule String do
defp do_reverse(nil, acc), do: IO.iodata_to_binary(acc)
@compile {:inline, duplicate: 2}
@doc """
Returns a string `subject` duplicated `n` times.
Inlined by the compiler.
## Examples
iex> String.duplicate("abc", 0)
@@ -1207,7 +1108,7 @@ defmodule String do
"""
@spec duplicate(t, non_neg_integer) :: t
def duplicate(subject, n) do
def duplicate(subject, n) when is_integer(n) and n >= 0 do
:binary.copy(subject, n)
end
@@ -1273,9 +1174,6 @@ defmodule String do
iex> String.valid?(<<0xFFFF :: 16>>)
false
iex> String.valid?(<<0xEF, 0xB7, 0x90>>)
true
iex> String.valid?("asd" <> <<0xFFFF :: 16>>)
false
@@ -1283,14 +1181,24 @@ defmodule String do
@spec valid?(t) :: boolean
def valid?(string)
noncharacters = Enum.to_list(0xFDD0..0xFDEF) ++
[0x0FFFE, 0x0FFFF, 0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF,
0x3FFFE, 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF,
0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, 0x8FFFF,
0x9FFFE, 0x9FFFF, 0x10FFFE, 0x10FFFF]
for noncharacter <- noncharacters do
def valid?(<<unquote(noncharacter)::utf8, _::binary>>), do: false
end
def valid?(<<_::utf8, t::binary>>), do: valid?(t)
def valid?(<<>>), do: true
def valid?(_), do: false
@doc false
# TODO: Remove on 2.0
# (hard-deprecated in elixir_dispatch)
def valid_character?(string) do
IO.warn "String.valid_character?/1 is deprecated, please use valid?/1 instead"
case string do
<<_::utf8>> -> valid?(string)
_ -> false
@@ -1302,8 +1210,8 @@ defmodule String do
The trait can be one of two options:
* `:valid` - the string is split into chunks of valid and invalid
character sequences
* `:valid` - the string is split into chunks of valid and invalid character
sequences
* `:printable` - the string is split into chunks of printable and
non-printable character sequences
@@ -1318,8 +1226,8 @@ defmodule String do
iex> String.chunk(<<?a, ?b, ?c, 0>>, :valid)
["abc\0"]
iex> String.chunk(<<?a, ?b, ?c, 0, 0xFFFF::utf16>>, :valid)
["abc\0", <<0xFFFF::utf16>>]
iex> String.chunk(<<?a, ?b, ?c, 0, 0x0FFFF::utf8>>, :valid)
["abc\0", <<0x0FFFF::utf8>>]
iex> String.chunk(<<?a, ?b, ?c, 0, 0x0FFFF::utf8>>, :printable)
["abc", <<0, 0x0FFFF::utf8>>]
@@ -1419,7 +1327,7 @@ defmodule String do
defdelegate next_grapheme_size(string), to: String.Unicode
@doc """
Returns the first grapheme from a UTF-8 string,
Returns the first grapheme from a utf8 string,
`nil` if the string is empty.
## Examples
@@ -1440,7 +1348,7 @@ defmodule String do
end
@doc """
Returns the last grapheme from a UTF-8 string,
Returns the last grapheme from a utf8 string,
`nil` if the string is empty.
## Examples
@@ -1464,7 +1372,7 @@ defmodule String do
defp do_last(nil, last_char), do: last_char
@doc """
Returns the number of Unicode graphemes in a UTF-8 string.
Returns the number of Unicode graphemes in a utf8 string.
## Examples
@@ -1479,7 +1387,7 @@ defmodule String do
defdelegate length(string), to: String.Unicode
@doc """
Returns the grapheme at the `position` of the given UTF-8 `string`.
Returns the grapheme at the `position` of the given utf8 `string`.
If `position` is greater than `string` length, then it returns `nil`.
## Examples
@@ -1507,7 +1415,7 @@ defmodule String do
end
def at(string, position) when is_integer(position) and position < 0 do
position = length(string) + position
position = length(string) - abs(position)
case position >= 0 do
true -> do_at(string, position)
false -> nil
@@ -1729,7 +1637,7 @@ defmodule String do
iex> String.ends_with? "language", ["youth", "elixir"]
false
An empty suffix will always match:
An empty string will always match:
iex> String.ends_with? "language", ""
true
@@ -1847,16 +1755,8 @@ defmodule String do
@doc """
Converts a string to an atom.
Warning: this function creates atoms dynamically and atoms are
not garbage collected. Therefore, `string` should not be an
untrusted value, such as input received from a socket or during
a web request. Consider using `to_existing_atom/1` instead.
By default, the maximum number of atoms is `1_048_576`. This limit
can be raised or lowered using the VM option `+t`.
The maximum atom size is of 255 characters. Prior to OTP 20,
only latin1 characters are allowed.
Currently Elixir does not support the conversion of strings
that contain Unicode codepoints greater than 0xFF.
Inlined by the compiler.
@@ -1874,8 +1774,8 @@ defmodule String do
@doc """
Converts a string to an existing atom.
The maximum atom size is of 255 characters. Prior to OTP 20,
only latin1 characters are allowed.
Currently Elixir does not support the conversion of strings
that contain Unicode codepoints greater than 0xFF.
Inlined by the compiler.
@@ -1929,9 +1829,10 @@ defmodule String do
@doc """
Returns a float whose text representation is `string`.
`string` must be the string representation of a float including a decimal point.
In order to parse a string without decimal point as a float then `Float.parse/1`
should be used. Otherwise, an `ArgumentError` will be raised.
`string` must be the string representation of a float.
If a string representation of an integer wants to be used,
then `Float.parse/1` should be used instead,
otherwise an argument error will be raised.
Inlined by the compiler.
@@ -1943,9 +1844,6 @@ defmodule String do
iex> String.to_float("3.0")
3.0
String.to_float("3")
#=> ** (ArgumentError) argument error
"""
@spec to_float(String.t) :: float
def to_float(string) do
@@ -2048,7 +1946,8 @@ defmodule String do
@doc """
Returns a keyword list that represents an edit script.
Check `List.myers_difference/2` for more information.
The algorithm is outlined in the
"An O(ND) Difference Algorithm and Its Variations" paper by E. Myers.
## Examples
@@ -2059,15 +1958,93 @@ defmodule String do
"""
@spec myers_difference(t, t) :: [{:eq | :ins | :del, t}] | nil
def myers_difference(string1, string2) do
List.myers_difference(graphemes(string1), graphemes(string2))
|> Enum.map(fn {kind, chars} ->
{kind, IO.iodata_to_binary(chars)}
end)
def myers_difference(str1, str2) do
{chars1, len1} = chars_and_length(str1)
{chars2, len2} = chars_and_length(str2)
path = {0, 0, chars1, chars2, []}
find_script(0, len1 + len2, [path])
end
# TODO: Remove by 2.0
# (hard-deprecated in elixir_dispatch)
defp find_script(envelope, max, _paths) when envelope > max do
nil
end
defp find_script(envelope, max, paths) do
case each_diagonal(-envelope, envelope, paths, []) do
{:done, edits} -> compact_reverse(edits, [])
{:next, paths} -> find_script(envelope + 1, max, paths)
end
end
defp compact_reverse([], acc), do: acc
defp compact_reverse([{kind, char} | rest], [{kind, chars} | acc]) do
compact_reverse(rest, [{kind, char <> chars} | acc])
end
defp compact_reverse([elem | rest], acc) do
compact_reverse(rest, [elem | acc])
end
defp each_diagonal(diag, limit, _paths, next_paths) when diag > limit do
{:next, Enum.reverse(next_paths)}
end
defp each_diagonal(diag, limit, paths, next_paths) do
{path, rest} = proceed_path(diag, limit, paths)
with {:cont, path} <- follow_snake(path) do
each_diagonal(diag + 2, limit, rest, [path | next_paths])
end
end
defp proceed_path(0, 0, [path]), do: {path, []}
defp proceed_path(diag, limit, [path | _] = paths) when diag == -limit do
{move_down(path), paths}
end
defp proceed_path(diag, limit, [path]) when diag == limit do
{move_right(path), []}
end
defp proceed_path(_diag, _limit, [path1, path2 | rest]) do
if elem(path1, 1) > elem(path2, 1) do
{move_right(path1), [path2 | rest]}
else
{move_down(path2), [path2 | rest]}
end
end
defp move_right({x, y, chars1, [char | rest], edits}) do
{x + 1, y, chars1, rest, [{:ins, char} | edits]}
end
defp move_right({x, y, chars1, [], edits}) do
{x + 1, y, chars1, [], edits}
end
defp move_down({x, y, [char | rest], chars2, edits}) do
{x, y + 1, rest, chars2, [{:del, char} | edits]}
end
defp move_down({x, y, [], chars2, edits}) do
{x, y + 1, [], chars2, edits}
end
defp follow_snake({x, y, [char | rest1], [char | rest2], edits}) do
follow_snake({x + 1, y + 1, rest1, rest2, [{:eq, char} | edits]})
end
defp follow_snake({_x, _y, [], [], edits}) do
{:done, edits}
end
defp follow_snake(path) do
{:cont, path}
end
# TODO: Deprecate by v1.5
@doc false
@spec to_char_list(t) :: charlist
def to_char_list(string), do: String.to_charlist(string)
+5 -10
View File
@@ -3,22 +3,17 @@ import Kernel, except: [to_string: 1]
defprotocol String.Chars do
@moduledoc ~S"""
The `String.Chars` protocol is responsible for
converting a structure to a binary (only if applicable).
converting a structure to a Binary (only if applicable).
The only function required to be implemented is
`to_string/1`, which does the conversion.
`to_string` which does the conversion.
The `to_string/1` function automatically imported
by `Kernel` invokes this protocol. String
interpolation also invokes `to_string/1` in its
The `to_string` function automatically imported
by Kernel invokes this protocol. String
interpolation also invokes `to_string` in its
arguments. For example, `"foo#{bar}"` is the same
as `"foo" <> to_string(bar)`.
"""
@doc """
Converts `term` to a string.
"""
@spec to_string(t) :: String.t
def to_string(term)
end
+81 -70
View File
@@ -40,7 +40,7 @@ defmodule StringIO do
{"", ">"}
"""
@spec open(binary, keyword) :: {:ok, pid}
@spec open(binary, Keyword.t) :: {:ok, pid}
def open(string, options \\ []) when is_binary(string) do
GenServer.start_link(__MODULE__, {string, options}, [])
end
@@ -216,12 +216,20 @@ defmodule StringIO do
## get_chars
defp get_chars(encoding, prompt, n, %{input: input} = s) do
defp get_chars(encoding, prompt, n,
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
case do_get_chars(input, encoding, n) do
{:error, _} = error ->
{error, s}
{result, input} ->
{result, state_after_read(s, input, prompt)}
s =
if capture_prompt do
%{s | output: <<output::binary, IO.chardata_to_string(prompt)::binary>>}
else
s
end
{result, %{s | input: input}}
end
end
@@ -255,83 +263,90 @@ defmodule StringIO do
## get_line
defp get_line(encoding, prompt, %{input: input} = s) do
case bytes_until_eol(input, encoding, 0) do
{:split, 0} ->
{:eof, state_after_read(s, "", prompt)}
{:split, count} ->
{result, remainder} = :erlang.split_binary(input, count)
defp get_line(encoding, prompt,
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
case :unicode.characters_to_list(input, encoding) do
{:error, _, _} ->
{{:error, :collect_line}, s}
{:incomplete, _, _} ->
{{:error, :collect_line}, s}
chars ->
{result, input} = do_get_line(chars, encoding)
{result, state_after_read(s, remainder, prompt)}
{:replace_split, count} ->
{result, remainder} = :erlang.split_binary(input, count)
s =
if capture_prompt do
%{s | output: <<output::binary, IO.chardata_to_string(prompt)::binary>>}
else
s
end
{binary_part(result, 0, byte_size(result) - 2) <> "\n", state_after_read(s, remainder, prompt)}
:error
-> {{:error, :collect_line}, s}
{result, %{s | input: input}}
end
end
defp do_get_line('', _encoding) do
{:eof, ""}
end
defp do_get_line(chars, encoding) do
{line, rest} = collect_line(chars)
{:unicode.characters_to_binary(line, encoding),
:unicode.characters_to_binary(rest, encoding)}
end
## get_until
defp get_until(encoding, prompt, mod, fun, args, %{input: input} = s) do
case do_get_until(input, encoding, mod, fun, args) do
{result, input, count} ->
input =
case input do
:eof -> ""
_ -> list_to_binary(input, encoding)
end
{get_until_result(result, encoding), state_after_read(s, input, prompt, count)}
:error ->
defp get_until(encoding, prompt, mod, fun, args,
%{input: input, output: output, capture_prompt: capture_prompt} = s) do
case :unicode.characters_to_list(input, encoding) do
{:error, _, _} ->
{:error, s}
{:incomplete, _, _} ->
{:error, s}
chars ->
{result, input, count} = do_get_until(chars, encoding, mod, fun, args)
input =
case input do
:eof -> ""
_ -> :unicode.characters_to_binary(input, encoding)
end
s =
if capture_prompt do
%{s | output: <<output::binary, :binary.copy(IO.chardata_to_string(prompt), count)::binary>>}
else
s
end
{result, %{s | input: input}}
end
end
defp do_get_until(chars, encoding, mod, fun, args, continuation \\ [], count \\ 0)
defp do_get_until("", encoding, mod, fun, args, continuation, count) do
defp do_get_until('', encoding, mod, fun, args, continuation, count) do
case apply(mod, fun, [continuation, :eof | args]) do
{:done, result, rest} ->
{result, rest, count + 1}
{:more, next_continuation} ->
do_get_until("", encoding, mod, fun, args, next_continuation, count + 1)
do_get_until('', encoding, mod, fun, args, next_continuation, count + 1)
end
end
defp do_get_until(chars, encoding, mod, fun, args, continuation, count) do
case bytes_until_eol(chars, encoding, 0) do
{r, c} when r in [:split, :replace_split] ->
{line, rest} = :erlang.split_binary(chars, c)
{line, rest} = collect_line(chars)
case apply(mod, fun, [continuation, binary_to_list(line, encoding) | args]) do
{:done, result, :eof} ->
{result, rest, count + 1}
{:done, result, extra} ->
{result, extra ++ binary_to_list(rest, encoding), count + 1}
{:more, next_continuation} ->
do_get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
end
:error ->
:error
case apply(mod, fun, [continuation, line | args]) do
{:done, result, :eof} ->
{result, rest, count + 1}
{:done, result, extra} ->
{result, extra ++ rest, count + 1}
{:more, next_continuation} ->
do_get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
end
end
defp binary_to_list(l, _) when is_list(l), do: l
defp binary_to_list(b, :unicode) when is_binary(b), do: to_charlist(b)
defp binary_to_list(b, :latin1) when is_binary(b), do: :binary.bin_to_list(b)
defp list_to_binary(b, _) when is_binary(b), do: b
defp list_to_binary(l, :unicode) when is_list(l), do: to_string(l)
defp list_to_binary(l, :latin1) when is_list(l), do: :binary.list_to_bin(l)
# From http://erlang.org/doc/apps/stdlib/io_protocol.html: Result can be any
# Erlang term, but if it is a list(), the I/O server can convert it to a binary().
defp get_until_result(l, encoding) when is_list(l), do: list_to_binary(l, encoding)
defp get_until_result(other, _), do: other
## io_requests
defp io_requests([r | rs], {:ok, s}) do
@@ -344,29 +359,25 @@ defmodule StringIO do
## helpers
defp state_after_read(state, remainder, prompt, count \\ 1)
defp state_after_read(%{capture_prompt: false} = s, remainder, _prompt, _count) do
%{s | input: remainder}
defp collect_line(chars) do
collect_line(chars, [])
end
defp state_after_read(%{capture_prompt: true, output: output} = s, remainder, prompt, count) do
%{s | input: remainder, output: <<output::binary, :binary.copy(IO.chardata_to_string(prompt), count)::binary>>}
defp collect_line([], stack) do
{:lists.reverse(stack), []}
end
defp bytes_until_eol("", _, count), do: {:split, count}
defp bytes_until_eol(<<"\r\n"::binary, _::binary>>, _, count), do: {:replace_split, count + 2}
defp bytes_until_eol(<<"\n"::binary, _::binary>>, _, count), do: {:split, count + 1}
defp bytes_until_eol(<<head::utf8, tail::binary>>, :unicode, count) do
bytes_until_eol(tail, :unicode, count + byte_size(<<head::utf8>>))
defp collect_line([?\r, ?\n | rest], stack) do
{:lists.reverse([?\n | stack]), rest}
end
defp bytes_until_eol(<<_, tail::binary>>, :latin1, count) do
bytes_until_eol(tail, :latin1, count + 1)
defp collect_line([?\n | rest], stack) do
{:lists.reverse([?\n | stack]), rest}
end
defp bytes_until_eol(<<_::binary>>, _, _), do: :error
defp collect_line([h | t], stack) do
collect_line(t, [h | stack])
end
defp io_reply(from, reply_as, reply) do
send from, {:io_reply, reply_as, reply}
+139 -575
View File
@@ -1,33 +1,27 @@
defmodule Supervisor do
@moduledoc ~S"""
A behaviour module for implementing supervisors.
A behaviour module for implementing supervision functionality.
A supervisor is a process which supervises other processes, which we
refer to as *child processes*. Supervisors are used to build a hierarchical
process structure called a *supervision tree*. Supervision trees provide
fault-tolerance and encapsulate how our applications start and shutdown.
A supervisor is a process which supervises other processes, which we refer
to as *child processes*. Supervisors are used to build a hierarchical process
structure called a *supervision tree*. Supervision trees are a nice way to
structure fault-tolerant applications.
A supervisor implemented using this module has a standard set
of interface functions and includes functionality for tracing and error
reporting.
reporting. It also fits into a supervision tree.
## Examples
In order to define a supervisor, we need to first define a child process
that will be supervised. As an example, we will define a GenServer that
represents a stack:
that is going to be supervised. In order to do so, we will define a GenServer
that represents a stack:
defmodule Stack do
use GenServer
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
## Callbacks
def init(stack) do
{:ok, stack}
def start_link(state, opts \\ []) do
GenServer.start_link(__MODULE__, state, opts)
end
def handle_call(:pop, _from, [h | t]) do
@@ -39,44 +33,39 @@ defmodule Supervisor do
end
end
The stack is a small wrapper around lists. It allows us to put
an element on the top of the stack, by prepending to the list,
and to get the top of the stack by pattern matching.
We can now define our supervisor and start it as follows:
We can now start a supervisor that will start and supervise our
stack process as follows:
# Import helpers for defining supervisors
import Supervisor.Spec
# Start the supervisor with the stack as a single child.
#
# The first element of the tuple is the module containing
# the child implementation, the second is the argument
# given to start_link, in this case a stack with `:hello`.
{:ok, pid} = Supervisor.start_link([
{Stack, [:hello]}
], strategy: :one_for_one)
# Supervise the Stack server which will be started with
# a single argument [:hello] and the default registered
# name of MyStack.
children = [
worker(Stack, [[:hello], [name: MyStack]])
]
# After started, we can query the supervisor for information
Supervisor.count_children(pid)
#=> %{active: 1, specs: 1, supervisors: 0, workers: 1}
# Start the supervisor with our child
{:ok, pid} = Supervisor.start_link(children, strategy: :one_for_one)
Notice that when starting the GenServer, we are registering it
with name `Stack`, which allows us to call it directly and get
what is on the stack:
with name `MyStack`, which allows us to call it directly and
get what is on the stack:
GenServer.call(Stack, :pop)
GenServer.call(MyStack, :pop)
#=> :hello
GenServer.cast(Stack, {:push, :world})
GenServer.cast(MyStack, {:push, :world})
#=> :ok
GenServer.call(Stack, :pop)
GenServer.call(MyStack, :pop)
#=> :world
However, there is a bug in our stack server. If we call `:pop` and
the stack is empty, it is going to crash because no clause matches:
GenServer.call(Stack, :pop)
** (exit) exited in: GenServer.call(Stack, :pop, 5000)
GenServer.call(:sup_stack, :pop)
** (exit) exited in: GenServer.call(MyStack, :pop, 5000)
Luckily, since the server is being supervised by a supervisor, the
supervisor will automatically start a new one, with the initial stack
@@ -90,178 +79,9 @@ defmodule Supervisor do
workers and supervisors as children, each of them with their specific
configuration, shutdown values, and restart strategies.
The rest of this document will cover how child processes are started,
how they can be specified, different supervision strategies and more.
## The initialization process
In the previous section, we have started a supervisor with one child:
Supervisor.start_link([
{Stack, [:hello]}
], strategy: :one_for_one)
The first argument given to `start_link` is a list of children.
In the example above, we have passed a tuple, where the child is
implemented by the `Stack` module and receives an initial argument
of `[:hello]` on `Stack.start_link/1`.
Generally speaking, starting the child process happens in three steps:
1. First the supervisor calls `Stack.child_spec([:hello])`. This
function must return a **child specification** which describes
how the `Stack` process is supervised. When you `use GenServer`,
a `child_spec/1` is automatically defined for you but we will see
when and how it can be configured. This function is called once
when the supervisor starts (or in case of hot code upgrades).
2. The **child specification** tells the supervisor which function
to invoke to start the child process. By default, it is the
`start_link/1` function, receiving the same argument and defined
in the same module as the `child_spec/1` function. This function
is called every time a new child process is necessary. For example,
when we crashed a `Stack` in the previous session,
`Stack.start_link([:hello])` was called once more to start a new stack.
3. Finally, `Stack.start_link/1` starts a new process that runs
`Stack.init/1`, which is responsible for setting a process that
will react to messages.
In summary, when the `Supervisor.start_link(children, opts)` is called,
it traverses the list of children and retrieves their `child_spec/1`.
Then each child specification describes how each child is started,
typically via the `start_link/1` function. The supervisor invokes the
`start_link/1` when it initializes and whenever the child process needs
to be restarted. The new process started by `start_link/1` often
executes the `init` callback as its first step. The `init` callback is
where we initialize and configure the child process.
## Child specification
The child specification describes how the supervisor should start and
supervise a child process. We have learned that, when we invoked
`use GenServer`, a `Stack.child_spec/1` was automatically defined for
us. Let's invoke it and see what it returns:
Stack.child_spec([:hello])
#=> %{
id: Stack,
start: {Stack, :start_link, [[:hello]]},
restart: :permanent,
shutdown: 5000,
type: :worker
}
The child specification contains 5 keys. The first two are required
and the remaining ones are optional:
* `:id` - a value used to identify the child specification
internally by the supervisor; defaults to the given module.
In case of conflicting `:id`, the supervisor will refuse
to initialize and require explicit IDs. This key is required.
* `:start` - a tuple with the module-function-args to be invoked
to start the child process. This key is required.
* `:restart` - an atom that defines when a terminated child process
should be restarted (see the "Restart values" section below).
This key is optional and defaults to `:permanent`.
* `:shutdown` - an atom that defines how a child process should be
terminated (see the "Shutdown values" section below). This key
is optional and defaults to `5000` if the type is `:worker` or
`:infinity` if the type is `:supervisor`.
* `:type` - if the child process is a `:worker` or a `:supervisor`.
This key is optional and defaults to `:worker`.
There is a sixth key, called `:modules`, which is rarely changed and
it is set automatically based on the value in `:start`.
Most times, the behaviour module you are implementing will take care
of setting up a proper `child_spec/1` for you. For example, `use Supervisor`
will define a `child_spec/1` where the `:type` is set to `:supervisor`
and the `:shutdown` is `:infinity`. Still, if you need to customize
a certain behaviour, you can do so by defining your own `child_spec/1`
function or by passing options on `use`. For example, to specify a
`GenServer` with a shutdown limit of 10 seconds (10_000 miliseconds),
one might do:
use GenServer, shutdown: 10_000
Let's understand what the `:shutdown` and `:restart` options control.
### Shutdown values (:shutdown)
The following shutdown values are supported in the `:shutdown` option:
* `:brutal_kill` - the child process is unconditionally terminated
using `Process.exit(child, :kill)`.
* any integer >= 0 - the amount of time in miliseconds that the
supervisor will wait for children to terminate after emitting a
`Process.exit(child, :shutdown)` signal. If the child process is
not trapping exits, the initial `:shutdown` signal will terminate
the child process immediately. If the child process is trapping
exits, it has the given amount of time in miliseconds to terminate.
If it doesn't terminate within the specified time, the child process
is unconditionally terminated by the supervisor via
`Process.exit(child, :kill)`.
* `:infinity` - works as an integer except the supervisor will wait
indefinitely for the child to terminate. If the child process is a
supervisor, the recommend value is `:infinity` to give the supervisor
and its children enough time to shutdown. This option can be used with
regular workers but doing so is discouraged and requires extreme care.
If not used carefully and the child process does not terminate, it means
your application will never terminate as well.
### Restart values (:restart)
The `:restart` option controls what the supervisor should consider to
be a successful termination or not. If the termination is successful,
the supervisor won't restart the child. If the child process crashed,
the supervisor will start a new one.
The following restart values are supported in the `:restart` option:
* `:permanent` - the child process is always restarted.
* `:temporary` - the child process is never restarted, regardless
of the supervision strategy.
* `:transient` - the child process is restarted only if it
terminates abnormally, i.e., with an exit reason other than
`:normal`, `:shutdown` or `{:shutdown, term}`.
For a more complete understanding of the exit reasons and their
impact, see the "Exit reasons" section next.
## Exit reasons
A supervisor restarts a child process depending on its `:restart`
configuration. For example, when `:restart` is set `:transient`, the
supervisr does not restart the child in case it exits with reason `:normal`,
`:shutdown` or `{:shutdown, term}`.
So one may ask: which exit reason should I choose when exiting? There are
three options:
* `:normal` - in such cases, the exit won't be logged, there is no restart
in transient mode, and linked processes do not exit
* `:shutdown` or `{:shutdown, term}` - in such cases, the exit won't be
logged, there is no restart in transient mode, and linked processes exit
with the same reason unless they're trapping exits
* any other term - in such cases, the exit will be logged, there are
restarts in transient mode, and linked processes exit with the same
reason unless they're trapping exits
Notice that supervisor that reached maximum restart intensity will exit with
`:shutdown` reason. In this case the supervisor will only be restarted if its
child specification was defined with the `:restart` option is set to `:permanent`
(the default).
The rest of this documentation will cover supervision strategies; also read
the documentation for the `Supervisor.Spec` module to learn about the
specification for workers and supervisors.
## Module-based supervisors
@@ -270,28 +90,22 @@ defmodule Supervisor do
explicitly defining a supervision module:
defmodule MyApp.Supervisor do
# Automatically defines child_spec/1
use Supervisor
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
def start_link do
Supervisor.start_link(__MODULE__, [])
end
def init(_arg) do
Supervisor.init([
{Stack, [:hello]}
], strategy: :one_for_one)
def init([]) do
children = [
worker(Stack, [[:hello]])
]
# supervise/2 is imported from Supervisor.Spec
supervise(children, strategy: :one_for_one)
end
end
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 children that are automatically initialized, we have
defined a supervisor alongside its `c:init/1` callback and manually
initialized the children by calling `Supervisor.init/2`, passing
the same arguments we would have given to `start_link/2`.
You may want to use a module-based supervisor if:
* You need to perform some particular action on supervisor
@@ -304,63 +118,10 @@ defmodule Supervisor do
requires the whole tree to be restarted in order to
perform such swaps.
Note `use Supervisor` defines a `child_spec/1` function, allowing
the defined module to be put under a supervision tree. The generated
`child_spec/1` can be customized with the following options:
## Strategies
* `:id` - the child specification id, defauts to the current module
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
* `:restart` - when the supervisor should be restarted, defaults to `:permanent`
## start_link/2, init/2 and strategies
So far we have started the supervisor passing a single child as a tuple
as well as a strategy called `:one_for_one`:
Supervisor.start_link([
{Stack, [:hello]}
], strategy: :one_for_one)
Or:
Supervisor.init([
{Stack, [:hello]}
], strategy: :one_for_one)
However, children can be specified in three different formats and
supervisors support different options. Let's formally define those.
The first argument given to `start_link/2` is a list of children which may
be either:
* a module - such as `Stack`. In this case, it is equivalent to passing
`{Stack, []}` (which means `Stack.child_spec/1` is invoked with an empty
keywords list)
* a tuple with a module as first element and the start argument as second -
such as `{Stack, [:hello]}`. When such format is used, the supervisor
will retrieve the child specification from the given module.
* a map representing the child specification itself - such as the child
specification map outlined in the previous section.
The second argument is a keyword list of options:
* `:strategy` - the restart strategy option. It can be either
`:one_for_one`, `:rest_for_one`, `:one_for_all`, or
`:simple_one_for_one`. See the "Strategies" section.
* `:max_restarts` - the maximum amount of restarts allowed in
a time frame. Defaults to `3`.
* `:max_seconds` - the time frame in which `:max_restarts` applies.
Defaults to `5`.
The `:strategy` option is required and by default a maximum of 3 restarts
is allowed within 5 seconds.
### Strategies
Supervisors support different supervision strategies (through the
`:strategy` option, as seen above):
Supervisors support different supervision strategies (through the `:strategy`
option, as seen above):
* `:one_for_one` - if a child process terminates, only that
process is restarted.
@@ -382,88 +143,91 @@ defmodule Supervisor do
## Simple one for one
The `:simple_one_for_one` supervisor is useful when you want to
dynamically start and stop supervised children. As an example,
let's start multiple agents dynamically to keep state.
The `:simple_one_for_one` supervisor is useful when you want to dynamically
start and stop supervised children. For example, imagine you want to
dynamically create multiple stacks. We can do so by defining a `:simple_one_for_one`
supervisor:
One important aspect in `:simple_one_for_one` supervisors is
that we often want to pass the `:start` arguments later on,
when starting the children dynamically, rather than when the
child specification is defined. In such cases, we should not do
# Import helpers for defining supervisors
import Supervisor.Spec
Supervisor.start_link [
{Agent, fn -> 0 end}
# This time, we don't pass any argument because
# the argument will be given when we start the child
children = [
worker(Stack, [], restart: :transient)
]
as the example above would force all agents to have the same state.
In such cases, we can use the `child_spec/2` function to build
and override the fields in a child specification:
# Start the supervisor with our one child
{:ok, sup_pid} = Supervisor.start_link(children, strategy: :simple_one_for_one)
# Override the :start field to have no args.
# The second argument has no effect thanks to it.
agent_spec =
Supervisor.child_spec(Agent, start: {Agent, :start_link, []})
There are a couple differences here:
# We start a supervisor with a simple one for one strategy.
# The agent won't be started now but later on.
{:ok, sup_pid} =
Supervisor.start_link([agent_spec], strategy: :simple_one_for_one)
* the simple one for one specification can define only one child which
works as a template for when we call `start_child/2`
# No child worker is active until start_child is called
Supervisor.count_children(sup_pid)
#=> %{active: 0, specs: 1, supervisors: 0, workers: 0}
* we have defined the child to have a restart strategy of `:transient`. This
means that, if the child process exits due to a `:normal`, `:shutdown`,
or `{:shutdown, term}` reason, it won't be restarted. This is useful
as it allows our workers to politely shutdown and be removed from the
`:simple_one_for_one` supervisor, without being restarted. You can find
more information about restart strategies in the documentation for the
`Supervisor.Spec` module
The simple one for one strategy can define only one child which works
as a template for when we call `start_child/2`.
With the supervisor defined, let's dynamically start stacks:
With the supervisor started, let's dynamically start agents:
{:ok, pid} = Supervisor.start_child(sup_pid, [[:hello, :world], []])
GenServer.call(pid, :pop) #=> :hello
GenServer.call(pid, :pop) #=> :world
{:ok, agent1} = Supervisor.start_child(sup_pid, [fn -> 0 end])
Agent.update(agent1, & &1 + 1)
Agent.get(agent1, & &1) #=> 1
{:ok, agent2} = Supervisor.start_child(sup_pid, [fn -> %{} end])
Agent.get(agent2, & &1) #=> %{}
{:ok, pid} = Supervisor.start_child(sup_pid, [[:something, :else], []])
GenServer.call(pid, :pop) #=> :something
GenServer.call(pid, :pop) #=> :else
Supervisor.count_children(sup_pid)
#=> %{active: 2, specs: 1, supervisors: 0, workers: 2}
## Exit reasons
From the example above, you may have noticed that the `:transient` restart
strategy for the worker does not restart the child in case it exits with
reason `:normal`, `:shutdown` or `{:shutdown, term}`.
So one may ask: which exit reason should I choose when exiting my worker?
There are three options:
* `:normal` - in such cases, the exit won't be logged, there is no restart
in transient mode, and linked processes do not exit
* `:shutdown` or `{:shutdown, term}` - in such cases, the exit won't be
logged, there is no restart in transient mode, and linked processes exit
with the same reason unless they're trapping exits
* any other term - in such cases, the exit will be logged, there are
restarts in transient mode, and linked processes exit with the same reason
unless they're trapping exits
## Name registration
A supervisor is bound to the same name registration rules as a `GenServer`.
Read more about these rules in the documentation for `GenServer`.
"""
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
defmacro __using__(_) do
quote location: :keep do
@behaviour Supervisor
import Supervisor.Spec
spec = [
id: opts[:id] || __MODULE__,
start: Macro.escape(opts[:start]) || quote(do: {__MODULE__, :start_link, [arg]}),
restart: opts[:restart] || :permanent,
type: :supervisor
]
@doc false
def child_spec(arg) do
%{unquote_splicing(spec)}
end
defoverridable child_spec: 1
@doc false
def init(arg)
end
end
@doc """
Callback invoked to start the supervisor and during hot code upgrades.
"""
# TODO: Support {:ok, [child_spec], Keyword.t}
# TODO: Document options here and update Supervisor.Spec
@callback init(args :: term) ::
{:ok, {:supervisor.sup_flags, [:supervisor.child_spec]}} |
{:ok, {:supervisor.sup_flags, [Supervisor.Spec.spec]}} |
:ignore
@typedoc "Return values of `start_link` functions"
@@ -479,45 +243,32 @@ defmodule Supervisor do
@typedoc "The Supervisor name"
@type name :: atom | {:global, term} | {:via, module, term}
@typedoc "Option values used by the `start*` functions"
@type option :: {:name, name} | flag()
@typedoc "Options used by the `start*` functions"
@type options :: [option, ...]
@type options :: [name: name,
strategy: Supervisor.Spec.strategy,
max_restarts: non_neg_integer,
max_seconds: non_neg_integer]
@typedoc "The supervisor reference"
@type supervisor :: pid | name | {atom, node}
@typedoc "Options given to `start_link/2` and `init/2`"
@type flag :: {:strategy, strategy} |
{:max_restarts, non_neg_integer} |
{:max_seconds, pos_integer}
@typedoc "Supported strategies"
@type strategy :: :simple_one_for_one | :one_for_one | :one_for_all | :rest_for_one
@typedoc "The supervisor specification"
@type child_spec :: :supervisor.child_spec()
@doc """
Starts a supervisor with the given children.
The children is a list of modules, 2-element tuples with module and
arguments or a map with the child specification. A strategy is required
to be provided through the `:strategy` option. See
"start_link/2, init/2 and strategies" for examples and other options.
A strategy is required to be provided through the `:strategy` option.
Furthermore, the `:max_restarts` and `:max_seconds` options can be
configured as described in the documentation for `Supervisor.Spec.supervise/2`.
The options can also be used to register a supervisor name.
The supported values are described under the "Name registration"
section in the `GenServer` module docs.
If the supervisor and its child processes are successfully spawned
(if the start function of each child process returns `{:ok, child}`,
If the supervisor and its child processes are successfully created
(i.e., if the start function of each child process returns `{:ok, child}`,
`{:ok, child, info}`, or `:ignore`) this function returns
`{:ok, pid}`, where `pid` is the PID of the supervisor. If the supervisor
is given a name and a process with the specified name already exists,
the function returns `{:error, {:already_started, pid}}`, where `pid`
is the PID of that process.
`{:ok, pid}`, where `pid` is the pid of the supervisor. If a process with the
specified name already exists, the function returns `{:error,
{:already_started, pid}}`, where `pid` is the pid of that process.
If the start function of any of the child processes fails or returns an error
tuple or an erroneous value, the supervisor first terminates with reason
@@ -528,193 +279,21 @@ defmodule Supervisor do
process and exits not only on crashes but also if the parent process exits
with `:normal` reason.
"""
@spec start_link([:supervisor.child_spec | {module, term} | module], options) :: on_start
@spec start_link([Supervisor.Spec.spec], options) :: on_start
def start_link(children, options) when is_list(children) do
{sup_opts, start_opts} = Keyword.split(options, [:strategy, :max_seconds, :max_restarts])
start_link(Supervisor.Default, {children, sup_opts}, start_opts)
spec = Supervisor.Spec.supervise(children, options)
start_link(Supervisor.Default, spec, options)
end
@doc """
Receives a list of children to initialize and a set of options.
Starts a supervisor module with the given `arg`.
This is typically invoked at the end of the `c:init/1` callback of
module-based supervisors. See the sections "Module-based supervisors"
and "start_link/2, init/2 and strategies" in the module
documentation for more information.
To start the supervisor, the `init/1` callback will be invoked in the given
`module`, with `arg` as its argument. The `init/1` callback must return a
supervisor specification which can be created with the help of the functions
in the `Supervisor.Spec` module (especially `Supervisor.Spec.supervise/2`).
This function returns a tuple containing the supervisor
flags and child specifications.
## Examples
def init(_arg) do
Supervisor.init([
{Stack, [:hello]}
], strategy: :one_for_one)
end
## Options
* `:strategy` - the restart strategy option. It can be either
`:one_for_one`, `:rest_for_one`, `:one_for_all`, or
`:simple_one_for_one`. You can learn more about strategies
in the `Supervisor` module docs.
* `:max_restarts` - the maximum amount of restarts allowed in
a time frame. Defaults to `3`.
* `:max_seconds` - the time frame in which `:max_restarts` applies.
Defaults to `5`.
The `:strategy` option is required and by default a maximum of 3 restarts
is allowed within 5 seconds. Check the `Supervisor` module for a detailed
description of the available strategies.
"""
@spec init([:supervisor.child_spec | {module, term} | module], flag) :: {:ok, tuple}
def init(children, options) when is_list(children) and is_list(options) do
unless strategy = options[:strategy] do
raise ArgumentError, "expected :strategy option to be given"
end
intensity = Keyword.get(options, :max_restarts, 3)
period = Keyword.get(options, :max_seconds, 5)
flags = %{strategy: strategy, intensity: intensity, period: period}
{:ok, {flags, Enum.map(children, &init_child/1)}}
end
defp init_child(module) when is_atom(module) do
init_child({module, []})
end
defp init_child({module, arg}) when is_atom(module) do
try do
module.child_spec(arg)
rescue
e in UndefinedFunctionError ->
case System.stacktrace do
[{^module, :child_spec, [^arg], _} | _] ->
raise ArgumentError, child_spec_error(module)
stack ->
reraise e, stack
end
end
end
defp init_child(map) when is_map(map) do
map
end
defp init_child({_, _, _, _, _, _} = tuple) do
tuple
end
defp init_child(other) do
raise ArgumentError, """
supervisors expect each child to be one of:
* a module
* a {module, arg} tuple
* a child specification as a map with at least the :id and :start fields
* or a tuple with 6 elements generated by Supervisor.Spec (deprecated)
Got: #{inspect other}
"""
end
defp child_spec_error(module) do
if Code.ensure_loaded?(module) do
"""
The module #{inspect module} was given as a child to a supervisor
but it does not implement child_spec/1.
If you own the given module, please define a child_spec/1 function
that receives an argument and returns a child specification as a map.
For example:
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
Note that "use Agent", "use GenServer" and so on automatically define
this function for you.
However, if you don't own the given module and it doesn't implement
child_spec/1, instead of passing the module name directly as a supervisor
child, you will have to pass a child specification as a map:
%{
id: #{inspect module},
start: {#{inspect module}, :start_link, [arg1, arg2]}
}
See the Supervisor documentation for more information.
"""
else
"The module #{inspect module} was given as a child to a supervisor but it does not exist."
end
end
@doc """
Builds and overrides a child specification.
Similar to `start_link/2` and `init/2`, it expects a
`module`, `{module, arg}` or a map as the child specification.
If a module is given, the specification is retrieved by calling
`module.child_spec(arg)`.
After the child specification is retrieved, the fields on `config`
are directly applied on the child spec. If `config` has keys that
do not map to any child specification field, an error is raised.
See the "Child specification" section in the module documentation
for all of the available keys for overriding.
## Examples
This function is often used to set an `:id` option when
the same module needs to be started multiple times in the
supervision tree:
Supervisor.child_spec({Agent, fn -> :ok end}, id: {Agent, 1})
#=> %{id: {Agent, 1},
start: {Agent, :start_link, [fn -> :ok end]}}
It may also be used when there is a need to change the number
of arguments when starting a module under a `:simple_one_for_one`
strategy, since most args may be given dynamically:
Supervisor.child_spec(Agent, start: {Agent, :start_link, []})
#=> %{id: Agent,
start: {Agent, :start_link, []}}
"""
@spec child_spec(child_spec() | {module, arg :: term} | module, keyword) :: child_spec()
def child_spec(module_or_map, overrides)
def child_spec({_, _, _, _, _, _} = tuple, _overrides) do
raise ArgumentError, "old tuple-based child specification #{inspect tuple} " <>
"is not supported in Supervisor.child_spec/2"
end
def child_spec(module_or_map, overrides) do
Enum.reduce overrides, init_child(module_or_map), fn
{key, value}, acc when key in [:id, :start, :restart, :shutdown, :type, :modules] ->
Map.put(acc, key, value)
{key, _value}, _acc ->
raise ArgumentError, "unknown key #{inspect key} in child specification override"
end
end
@doc """
Starts a supervisor process with the given `module` and `arg`.
To start the supervisor, the `c:init/1` callback will be invoked in the given
`module`, with `arg` as its argument. The `c:init/1` callback must return a
supervisor specification which can be created with the help of the `init/2`
function.
If the `c:init/1` callback returns `:ignore`, this function returns
If the `init/1` callback returns `:ignore`, this function returns
`:ignore` as well and the supervisor terminates with reason `:normal`.
If it fails or returns an incorrect value, this function returns
`{:error, term}` where `term` is a term with information about the
@@ -725,28 +304,15 @@ defmodule Supervisor do
section in the `GenServer` module docs.
"""
@spec start_link(module, term) :: on_start
@spec start_link(module, term, GenServer.options) :: on_start
@spec start_link(module, term, options) :: on_start
def start_link(module, arg, options \\ []) when is_list(options) do
case Keyword.get(options, :name) do
nil ->
:supervisor.start_link(module, arg)
atom when is_atom(atom) ->
:supervisor.start_link({:local, atom}, module, arg)
{:global, _term} = tuple ->
:supervisor.start_link(tuple, module, arg)
{:via, via_module, _term} = tuple when is_atom(via_module) ->
:supervisor.start_link(tuple, module, arg)
other ->
raise ArgumentError, """
expected :name option to be one of:
* nil
* atom
* {:global, term}
* {:via, module, term}
Got: #{inspect(other)}
"""
other when is_tuple(other) ->
:supervisor.start_link(other, module, arg)
end
end
@@ -768,11 +334,11 @@ defmodule Supervisor do
respectively.
If the child process start function returns `{:ok, child}` or `{:ok, child,
info}`, then child specification and PID are added to the supervisor and
info}`, then child specification and pid are added to the supervisor and
this function returns the same value.
If the child process start function returns `:ignore`, the child specification
is added to the supervisor, the PID is set to `:undefined` and this function
is added to the supervisor, the pid is set to `:undefined` and this function
returns `{:ok, :undefined}`.
If the child process start function returns an error tuple or an erroneous
@@ -780,21 +346,19 @@ defmodule Supervisor do
returns `{:error, error}` where `error` is a term containing information about
the error and child specification.
"""
# TODO: Once we add DynamicSupervisor, we need to enforce receiving
# a map here and deprecate the list and tuple formats.
@spec start_child(supervisor, :supervisor.child_spec | [term]) :: on_start_child
@spec start_child(supervisor, Supervisor.Spec.spec | [term]) :: on_start_child
def start_child(supervisor, child_spec_or_args) do
call(supervisor, {:start_child, child_spec_or_args})
end
@doc """
Terminates the given children, identified by PID or child id.
Terminates the given children, identified by pid or child id.
If the supervisor is not a `:simple_one_for_one`, the child id is expected
and the process, if there's one, is terminated; the child specification is
kept unless the child is temporary.
In case of a `:simple_one_for_one` supervisor, a PID is expected. If the child
In case of a `:simple_one_for_one` supervisor, a pid is expected. If the child
specification identifier is given instead of a `pid`, this function returns
`{:error, :simple_one_for_one}`.
@@ -803,10 +367,10 @@ defmodule Supervisor do
`delete_child/2` to remove the child specification.
If successful, this function returns `:ok`. If there is no child specification
for the given child id or there is no process with the given PID, this
for the given child id or there is no process with the given pid, this
function returns `{:error, :not_found}`.
"""
@spec terminate_child(supervisor, pid | term()) :: :ok | {:error, error}
@spec terminate_child(supervisor, pid | Supervisor.Spec.child_id) :: :ok | {:error, error}
when error: :not_found | :simple_one_for_one
def terminate_child(supervisor, pid_or_child_id) do
call(supervisor, {:terminate_child, pid_or_child_id})
@@ -824,7 +388,7 @@ defmodule Supervisor do
This operation is not supported by `:simple_one_for_one` supervisors.
"""
@spec delete_child(supervisor, term()) :: :ok | {:error, error}
@spec delete_child(supervisor, Supervisor.Spec.child_id) :: :ok | {:error, error}
when error: :not_found | :simple_one_for_one | :running | :restarting
def delete_child(supervisor, child_id) do
call(supervisor, {:delete_child, child_id})
@@ -840,9 +404,9 @@ defmodule Supervisor do
when the child terminates, and thus it is not possible to restart such children.
If the child process start function returns `{:ok, child}` or `{:ok, child, info}`,
the PID is added to the supervisor and this function returns the same value.
the pid is added to the supervisor and this function returns the same value.
If the child process start function returns `:ignore`, the PID remains set to
If the child process start function returns `:ignore`, the pid remains set to
`:undefined` and this function returns `{:ok, :undefined}`.
This function may return an error with an appropriate error tuple if the
@@ -854,7 +418,7 @@ defmodule Supervisor do
This operation is not supported by `:simple_one_for_one` supervisors.
"""
@spec restart_child(supervisor, term()) ::
@spec restart_child(supervisor, Supervisor.Spec.child_id) ::
{:ok, child} | {:ok, child, term} | {:error, error}
when error: :not_found | :simple_one_for_one | :running | :restarting | term
def restart_child(supervisor, child_id) do
@@ -872,7 +436,7 @@ defmodule Supervisor do
* `id` - as defined in the child specification or `:undefined` in the case
of a `simple_one_for_one` supervisor
* `child` - the PID of the corresponding child process, `:restarting` if the
* `child` - the pid of the corresponding child process, `:restarting` if the
process is about to be restarted, or `:undefined` if there is no such
process
@@ -882,10 +446,10 @@ defmodule Supervisor do
"""
@spec which_children(supervisor) ::
[{term() | :undefined,
child | :restarting,
:worker | :supervisor,
:supervisor.modules}]
[{Supervisor.Spec.child_id | :undefined,
child | :restarting,
Supervisor.Spec.worker,
Supervisor.Spec.modules}]
def which_children(supervisor) do
call(supervisor, :which_children)
end
@@ -915,7 +479,7 @@ defmodule Supervisor do
end
@doc """
Synchronously stops the given supervisor with the given `reason`.
Stops the given supervisor with the given `reason`.
It returns `:ok` if the supervisor terminates with the given
reason. If it terminates with another reason, the call exits.
+8 -2
View File
@@ -1,7 +1,13 @@
defmodule Supervisor.Default do
@moduledoc false
def init({children, opts}) do
Supervisor.init(children, opts)
@doc """
Supervisor callback that simply returns the given args.
This is the supervisor used by `Supervisor.start_link/2`
and others.
"""
def init(args) do
args
end
end
+11 -27
View File
@@ -1,9 +1,5 @@
defmodule Supervisor.Spec do
@moduledoc """
NOTE: The functions in this module are deprecated and they do not
work with the module-based child specs introduced in Elixir v1.5.
Please see the `Supervisor` documentation instead.
Convenience functions for defining supervisor specifications.
## Example
@@ -42,7 +38,7 @@ defmodule Supervisor.Spec do
Notice in this case we don't have to explicitly import
`Supervisor.Spec` as `use Supervisor` automatically does so.
Defining a module-based supervisor can be useful, for example,
to perform initialization tasks in the `c:init/1` callback.
to perform initialization tasks in the `init/1` callback.
## Supervisor and worker options
@@ -80,16 +76,12 @@ defmodule Supervisor.Spec do
terminates abnormally, i.e., with an exit reason other than
`:normal`, `:shutdown` or `{:shutdown, term}`
Notice that supervisor that reached maximum restart intensity will exit with `:shutdown` reason.
In this case the supervisor will only be restarted if its child specification was defined with
the `:restart` option is set to `:permanent` (the default).
### Shutdown values (:shutdown)
The following shutdown values are supported in the `:shutdown` option:
* `:brutal_kill` - the child process is unconditionally terminated
using `Process.exit(child, :kill)`
using `exit(child, :kill)`
* `:infinity` - if the child process is a supervisor, this is a mechanism
to give the subtree enough time to shutdown; it can also be used with
@@ -104,8 +96,7 @@ defmodule Supervisor.Spec do
"""
# TODO: Deprecate all functions in this module on Elixir v1.8.
# Also deprecate entry in Supervisor.Default.
# TODO: Update and provide a digest of strategies once we include DynamicSupervisor.
@typedoc "Supported strategies"
@type strategy :: :simple_one_for_one | :one_for_one | :one_for_all | :rest_for_one
@@ -114,7 +105,7 @@ defmodule Supervisor.Spec do
@type restart :: :permanent | :transient | :temporary
@typedoc "Supported shutdown values"
@type shutdown :: timeout | :brutal_kill
@type shutdown :: :brutal_kill | :infinity | non_neg_integer
@typedoc "Supported worker values"
@type worker :: :worker | :supervisor
@@ -138,7 +129,7 @@ defmodule Supervisor.Spec do
supervise and a set of options.
Returns a tuple containing the supervisor specification. This tuple can be
used as the return value of the `c:init/1` callback when implementing a
used as the return value of the `init/1` callback when implementing a
module-based supervisor.
## Examples
@@ -164,7 +155,9 @@ defmodule Supervisor.Spec do
"""
@spec supervise([spec], strategy: strategy,
max_restarts: non_neg_integer,
max_seconds: pos_integer) :: {:ok, tuple}
max_seconds: non_neg_integer) :: {:ok, tuple}
# TODO: Make it return a tuple of format {:ok, children, opts}
# TODO: Deprecate once the new tuple format has been established
def supervise(children, options) do
unless strategy = options[:strategy] do
raise ArgumentError, "expected :strategy option to be given"
@@ -173,21 +166,10 @@ defmodule Supervisor.Spec do
maxR = Keyword.get(options, :max_restarts, 3)
maxS = Keyword.get(options, :max_seconds, 5)
assert_unique_ids(Enum.map(children, &get_id/1))
assert_unique_ids(Enum.map(children, &elem(&1, 0)))
{:ok, {{strategy, maxR, maxS}, children}}
end
defp get_id({id, _, _, _, _, _}) do
id
end
defp get_id(other) do
raise ArgumentError,
"invalid tuple specification given to supervise/2. If you are trying to use " <>
"the map child specification that is part of the Elixir v1.5, use Supervisor.init/2 " <>
"instead of Supervisor.Spec.supervise/2. See the Supervisor module for more information. " <>
"Got: #{inspect other}"
end
defp assert_unique_ids([id | rest]) do
if id in rest do
raise ArgumentError,
@@ -251,6 +233,7 @@ defmodule Supervisor.Spec do
child(:supervisor, module, args, options)
end
# TODO: Do and expose proper child validation
defp child(type, module, args, options) do
id = Keyword.get(options, :id, module)
modules = Keyword.get(options, :modules, modules(module))
@@ -262,6 +245,7 @@ defmodule Supervisor.Spec do
restart, shutdown, type, modules}
end
# TODO: Remove GenEvent when there is no more GenEvent v2.0
defp modules(GenEvent), do: :dynamic
defp modules(module), do: [module]
end
+41 -116
View File
@@ -53,7 +53,7 @@ defmodule System do
the time, all calculations are done in the `:native` unit, to
avoid loss of precision, with `convert_time_unit/3` being
invoked at the end to convert to a specific time unit like
`:millisecond` or `:microsecond`. See the `t:time_unit/0` type for
milliseconds or microseconds. See the `t:time_unit/0` type for
more information.
For a more complete rundown on the VM support for different
@@ -65,30 +65,25 @@ defmodule System do
@typedoc """
The time unit to be passed to functions like `monotonic_time/1` and others.
The `:second`, `:millisecond`, `:microsecond` and `:nanosecond` time
The `:seconds`, `:milliseconds`, `:microseconds` and `:nanoseconds` time
units controls the return value of the functions that accept a time unit.
A time unit can also be a strictly positive integer. In this case, it
represents the "parts per second": the time will be returned in `1 /
parts_per_second` seconds. For example, using the `:millisecond` time unit
parts_per_second` seconds. For example, using the `:milliseconds` time unit
is equivalent to using `1000` as the time unit (as the time will be returned
in 1/1000 seconds - milliseconds).
Keep in mind the Erlang API prior to version 19.1 will use `:milli_seconds`,
`:micro_seconds` and `:nano_seconds` as time units although Elixir normalizes
their spelling to match the SI convention.
Keep in mind the Erlang API will use `:milli_seconds`, `:micro_seconds`
and `:nano_seconds` as time units although Elixir normalizes their spelling
to match the SI convention.
"""
@type time_unit ::
:second
| :millisecond
| :microsecond
| :nanosecond
| pos_integer
# TODO: Deprecate these in Elixir 2.0
| :seconds
:seconds
| :milliseconds
| :microseconds
| :nanoseconds
| pos_integer
@base_dir :filename.join(__DIR__, "../../..")
@version_file :filename.join(@base_dir, "VERSION")
@@ -129,7 +124,7 @@ defmodule System do
|> strip
end
defp revision, do: get_revision()
defp revision, do: get_revision
# Get the date at compilation time.
defmacrop get_date do
@@ -157,7 +152,7 @@ defmodule System do
Returns Elixir's version as binary.
"""
@spec version() :: String.t
def version, do: get_version()
def version, do: get_version
@doc """
Elixir build information.
@@ -166,21 +161,21 @@ defmodule System do
"""
@spec build_info() :: map
def build_info do
%{build: build(),
date: get_date(),
revision: revision(),
version: version()}
%{build: build,
date: get_date,
revision: revision,
version: version}
end
# Returns a string of the build info
defp build do
{:ok, v} = Version.parse(version())
{:ok, v} = Version.parse(version)
cond do
([] == v.pre) or ("" == revision()) ->
version()
([] == v.pre) or ("" == revision) ->
version
true ->
"#{version()} (#{revision()})"
"#{version} (#{revision})"
end
end
@@ -220,7 +215,7 @@ defmodule System do
defp fix_drive_letter([l, ?:, ?/ | rest] = original) when l in ?A..?Z do
case :os.type() do
{:win32, _} -> [l + ?a - ?A, ?:, ?/ | rest]
{:win32, _} -> [l+?a-?A, ?:, ?/ | rest]
_ -> original
end
end
@@ -233,7 +228,7 @@ defmodule System do
Returns the current working directory or raises `RuntimeError`.
"""
def cwd! do
cwd() ||
cwd ||
raise RuntimeError, message: "could not get a current working directory, the current location is not accessible"
end
@@ -253,7 +248,7 @@ defmodule System do
instead of returning `nil` if no user home is set.
"""
def user_home! do
user_home() ||
user_home ||
raise RuntimeError, message: "could not find the user home, please set the HOME environment variable"
end
@@ -286,7 +281,7 @@ defmodule System do
instead of returning `nil` if no temp dir is set.
"""
def tmp_dir! do
tmp_dir() ||
tmp_dir ||
raise RuntimeError, message: "could not get a writable temporary directory, " <>
"please set the TMPDIR environment variable"
end
@@ -339,8 +334,6 @@ defmodule System do
"""
@spec find_executable(binary) :: binary | nil
def find_executable(program) when is_binary(program) do
assert_no_null_byte!(program, "System.find_executable/1")
case :os.find_executable(String.to_charlist(program)) do
false -> nil
other -> List.to_string(other)
@@ -348,10 +341,10 @@ defmodule System do
end
@doc """
Returns all system environment variables.
System environment variables.
The returned value is a map containing name-value pairs.
Variable names and their values are strings.
Returns a list of all environment variables. Each variable is given as a
`{name, value}` tuple where both `name` and `value` are strings.
"""
@spec get_env() :: %{optional(String.t) => String.t}
def get_env do
@@ -363,13 +356,13 @@ defmodule System do
end
@doc """
Returns the value of the given environment variable.
Environment variable value.
The returned value of the environment variable
`varname` is a string, or `nil` if the environment
Returns the value of the environment variable
`varname` as a binary, or `nil` if the environment
variable is undefined.
"""
@spec get_env(String.t) :: String.t | nil
@spec get_env(binary) :: binary | nil
def get_env(varname) when is_binary(varname) do
case :os.getenv(String.to_charlist(varname)) do
false -> nil
@@ -395,13 +388,8 @@ defmodule System do
"""
@spec put_env(binary, binary) :: :ok
def put_env(varname, value) when is_binary(varname) and is_binary(value) do
case :binary.match(varname, "=") do
{_, _} ->
raise ArgumentError, "cannot execute System.put_env/2 for key with \"=\", got: #{inspect varname}"
:nomatch ->
:os.putenv String.to_charlist(varname), String.to_charlist(value)
:ok
end
:os.putenv String.to_charlist(varname), String.to_charlist(value)
:ok
end
@doc """
@@ -440,13 +428,10 @@ defmodule System do
end
@doc """
Immediately halts the Erlang runtime system.
Halts the Erlang runtime system.
Terminates the Erlang runtime system without properly shutting down
applications and ports. Please see `stop/1` for a careful shutdown of the
system.
`status` must be a non-negative integer, the atom `:abort` or a binary.
Halts the Erlang runtime system where the argument `status` must be a
non-negative integer, the atom `:abort` or a binary.
* If an integer, the runtime system exits with the integer value which
is returned to the operating system.
@@ -469,6 +454,7 @@ defmodule System do
System.halt(:abort)
"""
@spec halt() :: no_return
@spec halt(non_neg_integer | binary | :abort) :: no_return
def halt(status \\ 0)
@@ -480,37 +466,6 @@ defmodule System do
:erlang.halt(String.to_charlist(status))
end
@doc """
Carefully stops the Erlang runtime system.
All applications are taken down smoothly, all code is unloaded, and all ports
are closed before the system terminates by calling `halt/1`.
`status` must be a non-negative integer value which is returned by the
runtime system to the operating system.
Note that on many platforms, only the status codes 0-255 are supported
by the operating system.
For more information, see [`:init.stop/1`](http://erlang.org/doc/man/init.html#stop-1).
## Examples
System.stop(0)
System.stop(1)
"""
@spec stop(non_neg_integer | binary) :: no_return
def stop(status \\ 0)
def stop(status) when is_integer(status) do
:init.stop(status)
end
def stop(status) when is_binary(status) do
:init.stop(String.to_charlist(status))
end
@doc ~S"""
Executes the given `command` with `args`.
@@ -528,13 +483,6 @@ defmodule System do
This function returns a tuple containing the collected result
and the command exit status.
Internally, this function uses a `Port` for interacting with the
outside world. However, if you plan to run a long-running program,
ports guarantee stdin/stdout devices will be closed but it does not
automatically terminate the program. The documentation for the
`Port` module describes this problem and possible solutions under
the "Zombie processes" section.
## Examples
iex> System.cmd "echo", ["hello"]
@@ -552,7 +500,7 @@ defmodule System do
* `:into` - injects the result into the given collectable, defaults to `""`
* `:cd` - the directory to run the command in
* `:env` - an enumerable of tuples containing environment key-value as binary
* `:arg0` - sets the command arg0
* `:arg0` - set the command arg0
* `:stderr_to_stdout` - redirects stderr to stdout when `true`
* `: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
@@ -592,10 +540,9 @@ defmodule System do
redirecting and so on, please check
[`:os.cmd/1`](http://www.erlang.org/doc/man/os.html#cmd-1).
"""
@spec cmd(binary, [binary], keyword) ::
@spec cmd(binary, [binary], Keyword.t) ::
{Collectable.t, exit_status :: non_neg_integer}
def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
assert_no_null_byte!(command, "System.cmd/3")
cmd = String.to_charlist(command)
cmd =
@@ -722,12 +669,12 @@ defmodule System do
The result is rounded via the floor function.
`convert_time_unit/3` accepts an additional time unit (other than the
ones in the `t:time_unit/0` type) called `:native`. `:native` is the time
ones in the `time_unit` type) called `:native`. `:native` is the time
unit used by the Erlang runtime system. It's determined when the runtime
starts and stays the same until the runtime is stopped. To determine what
the `:native` unit amounts to in a system, you can call this function to
convert 1 second to the `:native` time unit (i.e.,
`System.convert_time_unit(1, :second, :native)`).
`System.convert_time_unit(1, :seconds, :native)`).
"""
@spec convert_time_unit(integer, time_unit | :native, time_unit | :native) :: integer
def convert_time_unit(time, from_unit, to_unit) do
@@ -841,29 +788,8 @@ defmodule System do
:erlang.unique_integer(modifiers)
end
defp assert_no_null_byte!(binary, operation) do
case :binary.match(binary, "\0") do
{_, _} ->
raise ArgumentError, "cannot execute #{operation} for program with null byte, got: #{inspect binary}"
:nomatch ->
binary
end
end
defp normalize_time_unit(:native),
do: :native
# TODO: Remove these mappings once Elixir requires Erlang/OTP 19.1
defp normalize_time_unit(:second),
do: :seconds
defp normalize_time_unit(:millisecond),
do: :milli_seconds
defp normalize_time_unit(:microsecond),
do: :micro_seconds
defp normalize_time_unit(:nanosecond),
do: :nano_seconds
# TODO: Warn on Elixir 1.5
defp normalize_time_unit(:seconds),
do: :seconds
defp normalize_time_unit(:milliseconds),
@@ -872,7 +798,6 @@ defmodule System do
do: :micro_seconds
defp normalize_time_unit(:nanoseconds),
do: :nano_seconds
defp normalize_time_unit(unit) when is_integer(unit) and unit > 0,
do: unit
@@ -884,8 +809,8 @@ defmodule System do
defp normalize_time_unit(other) do
raise ArgumentError,
"unsupported time unit. Expected :second, :millisecond, " <>
":microsecond, :nanosecond, or a positive integer, " <>
"unsupported time unit. Expected :seconds, :milliseconds, " <>
":microseconds, :nanoseconds, or a positive integer, " <>
"got #{inspect other}"
end
end
+58 -247
View File
@@ -33,7 +33,7 @@ defmodule Task do
There are two important things to consider when using `async`:
1. If you are using async tasks, you **must await** a reply
1. If you are using async tasks, you must await a reply
as they are *always* sent. If you are not expecting a reply,
consider using `Task.start_link/1` detailed below.
@@ -56,51 +56,28 @@ defmodule Task do
## Supervised tasks
It is also possible to spawn a task under a supervisor.
It is often done by defining the task in its own module:
It is also possible to spawn a task under a supervisor
with `start_link/1` and `start_link/3`:
defmodule MyTask do
use Task
Task.start_link(fn -> IO.puts "ok" end)
def start_link(arg) do
Task.start_link(__MODULE__, :run, [arg])
end
Such tasks can be mounted in your supervision tree as:
def run(arg) do
# ...
end
end
import Supervisor.Spec
And then passing it to the supervisor:
Supervisor.start_link([MyTask])
children = [
worker(Task, [fn -> IO.puts "ok" end])
]
Since these tasks are supervised and not directly linked to
the caller, they cannot be awaited on. Note `start_link/1`,
unlike `async/1`, returns `{:ok, pid}` (which is the result
expected by supervisors).
unlike `async/1`, returns `{:ok, pid}` (which is
the result expected by supervision trees).
Note `use Task` defines a `child_spec/1` function, allowing the
defined module to be put under a supervision tree. The generated
`child_spec/1` can be customized with the following options:
* `:id` - the child specification id, defauts to the current module
* `:start` - how to start the child process (defaults to calling `__MODULE__.start_link/1`)
* `:restart` - when the child should be restarted, defaults to `:temporary`
* `:shutdown` - how to shut down the child
Opposite to `GenServer`, `Agent` and `Supervisor`, a Task has
a default `:restart` of `:temporary`. This means the task will
not be restarted even if it crashes. If you desire the task to
be restarted for non-successful exists, do:
use Task, restart: :transient
If you want the task to always be restarted:
use Task, restart: :permanent
See the `Supervisor` docs for more information.
By default, most supervision strategies will try to restart
a worker after it exits regardless of the reason. If you design the
task to terminate normally (as in the example with `IO.puts/2` above),
consider passing `restart: :transient` in the options to `worker/3`.
## Dynamically supervised tasks
@@ -118,9 +95,11 @@ defmodule Task do
However, in the majority of cases, you want to add the task supervisor
to your supervision tree:
Supervisor.start_link([
{Task.Supervisor, name: MyApp.TaskSupervisor}
])
import Supervisor.Spec
children = [
supervisor(Task.Supervisor, [[name: MyApp.TaskSupervisor]])
]
Now you can dynamically start supervised tasks:
@@ -148,8 +127,8 @@ defmodule Task do
Task.Supervisor.async({MyApp.DistSupervisor, :remote@local},
MyMod, :my_fun, [arg1, arg2, arg3])
Note that, when working with distributed tasks, one should use the `Task.Supervisor.async/4` function
that expects explicit module, function and arguments, instead of `Task.Supervisor.async/2` that
Note that, when working with distributed tasks, one should use the `async/4` function
that expects explicit module, function and arguments, instead of `async/2` that
works with anonymous functions. That's because anonymous functions expect
the same module version to exist on all involved nodes. Check the `Agent` module
documentation for more information on distributed processes as the limitations
@@ -173,41 +152,10 @@ defmodule Task do
@type t :: %__MODULE__{}
@doc false
def child_spec(arg) do
%{
id: Task,
start: {Task, :start_link, [arg]},
restart: :temporary
}
end
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
spec = [
id: opts[:id] || __MODULE__,
start: Macro.escape(opts[:start]) || quote(do: {__MODULE__, :start_link, [arg]}),
restart: opts[:restart] || :temporary,
shutdown: opts[:shutdown] || 5000,
type: :worker
]
@doc false
def child_spec(arg) do
%{unquote_splicing(spec)}
end
defoverridable child_spec: 1
end
end
@doc """
Starts a process linked to the current process.
This is often used to start the process as part of a supervision tree.
Starts a task as part of a supervision tree.
"""
@spec start_link((() -> any)) :: {:ok, pid}
@spec start_link(fun) :: {:ok, pid}
def start_link(fun) do
start_link(:erlang, :apply, [fun, []])
end
@@ -217,7 +165,7 @@ defmodule Task do
"""
@spec start_link(module, atom, [term]) :: {:ok, pid}
def start_link(mod, fun, args) do
Task.Supervised.start_link(get_info(self()), {mod, fun, args})
Task.Supervised.start_link(get_info(self), {mod, fun, args})
end
@doc """
@@ -227,7 +175,7 @@ defmodule Task do
(i.e. no interest in the returned result) and it should not
be linked to the current process.
"""
@spec start((() -> any)) :: {:ok, pid}
@spec start(fun) :: {:ok, pid}
def start(fun) do
start(:erlang, :apply, [fun, []])
end
@@ -241,7 +189,7 @@ defmodule Task do
"""
@spec start(module, atom, [term]) :: {:ok, pid}
def start(mod, fun, args) do
Task.Supervised.start(get_info(self()), {mod, fun, args})
Task.Supervised.start(get_info(self), {mod, fun, args})
end
@doc """
@@ -256,7 +204,7 @@ defmodule Task do
See also `async/3`.
"""
@spec async((() -> any)) :: t
@spec async(fun) :: t
def async(fun) do
async(:erlang, :apply, [fun, []])
end
@@ -309,7 +257,7 @@ defmodule Task do
process immune to not only exits from the task but from
any other processes.
Moreover, even when trapping exits, calling `await` will
Moreover, even when trapping exists, calling `await` will
still exit if the task has terminated without sending its
result back.
@@ -334,112 +282,16 @@ defmodule Task do
%Task{pid: pid, ref: ref, owner: owner}
end
@doc """
Returns a stream that runs the given `module`, `function`, and `args`
concurrently on each item in `enumerable`.
Each item will be prepended to the given `args` and processed by its
own task. The tasks will be linked to an intermediate process that is
then linked to the current process. This means a failure in a task
terminates the current process and a failure in the current process
terminates all tasks.
When streamed, each task will emit `{:ok, value}` upon successful
completion or `{:exit, reason}` if the caller is trapping exits.
Results are emitted in the same order as the original `enumerable`.
The level of concurrency can be controlled via the `:max_concurrency`
option and defaults to `System.schedulers_online/0`. A timeout
can also be given as an option representing the maximum amount of
time to wait without a task reply.
Finally, consider using `Task.Supervisor.async_stream/6` to start tasks
under a supervisor. If you find yourself trapping exits to handle exits
inside the async stream, consider using `Task.Supervisor.async_stream_nolink/6`
to start tasks that are not linked to the current process.
## Options
* `:max_concurrency` - sets the maximum number of tasks to run
at the same time. Defaults to `System.schedulers_online/0`.
* `:ordered` - whether the results should be returned in the same order
as the input stream. This option is useful when you have large
streams and don't want to buffer results before they are delivered.
Defaults to `true`.
* `:timeout` - the maximum amount of time (in milliseconds) each
task is allowed to execute for. Defaults to `5000`.
* `:on_timeout` - what do to when a task times out. The possible
values are:
* `:exit` (default) - the process that spawned the tasks exits.
* `:kill_task` - the task that timed out is killed. The value
emitted for that task is `{:exit, :timeout}`.
## Example
Let's build a stream and then enumerate it:
stream = Task.async_stream(collection, Mod, :expensive_fun, [])
Enum.to_list(stream)
The concurrency can be increased or decreased using the `:max_concurrency`
option. For example, if the tasks are IO heavy, the value can be increased:
max_concurrency = System.schedulers_online * 2
stream = Task.async_stream(collection, Mod, :expensive_fun, [], max_concurrency: max_concurrency)
Enum.to_list(stream)
"""
@spec async_stream(Enumerable.t, module, atom, [term], keyword) :: Enumerable.t
def async_stream(enumerable, module, function, args, options \\ [])
when is_atom(module) and is_atom(function) and is_list(args) do
build_stream(enumerable, {module, function, args}, options)
defp get_info(self) do
{node(),
case Process.info(self, :registered_name) do
{:registered_name, []} -> self()
{:registered_name, name} -> name
end}
end
@doc """
Returns a stream that runs the given function `fun` concurrently
on each item in `enumerable`.
Each `enumerable` item is passed as argument to the given function `fun` and
processed by its own task. The tasks will be linked to the current process,
similarly to `async/1`.
## Example
Count the codepoints in each string asynchronously, then add the counts together using reduce.
iex> strings = ["long string", "longer string", "there are many of these"]
iex> stream = Task.async_stream(strings, fn text -> text |> String.codepoints |> Enum.count end)
iex> Enum.reduce(stream, 0, fn {:ok, num}, acc -> num + acc end)
47
See `async_stream/5` for discussion, options, and more examples.
"""
@spec async_stream(Enumerable.t, (term -> term), keyword) :: Enumerable.t
def async_stream(enumerable, fun, options \\ []) when is_function(fun, 1) do
build_stream(enumerable, fun, options)
end
defp build_stream(enumerable, fun, options) do
&Task.Supervised.stream(enumerable, &1, &2, fun, options, fn owner, mfa ->
{:link, Task.Supervised.spawn_link(owner, get_info(owner), mfa)}
end)
end
# Returns a tuple with the node where this is executed and either the
# registered name of the given pid or the pid of where this is executed. Used
# when exiting from tasks to print out from where the task was started.
defp get_info(pid) do
self_or_name =
case Process.info(pid, :registered_name) do
{:registered_name, []} -> self()
{:registered_name, name} -> name
end
{node(), self_or_name}
end
@doc """
Awaits a task reply and returns it.
Awaits a task reply.
A timeout, in milliseconds, can be given with default value
of `5000`. In case the task process dies, this function will
@@ -462,14 +314,7 @@ defmodule Task do
It is not recommended to `await` a long-running task inside an OTP
behaviour such as `GenServer`. Instead, you should match on the message
coming from a task inside your `GenServer.handle_info/2` callback.
## Examples
iex> task = Task.async(fn -> 1 + 1 end)
iex> Task.await(task)
2
coming from a task inside your `handle_info` callback.
"""
@spec await(t, timeout) :: term | no_return
def await(task, timeout \\ 5000)
@@ -494,8 +339,12 @@ defmodule Task do
@doc false
# TODO: Remove on 2.0
# (hard-deprecated in elixir_dispatch)
def find(tasks, {ref, reply}) when is_reference(ref) do
def find(tasks, msg) do
IO.warn "Task.find/2 is deprecated, please match on the message directly"
do_find(tasks, msg)
end
defp do_find(tasks, {ref, reply}) when is_reference(ref) do
Enum.find_value tasks, fn
%Task{ref: ^ref} = task ->
Process.demonitor(ref, [:flush])
@@ -505,18 +354,18 @@ defmodule Task do
end
end
def find(tasks, {:DOWN, ref, _, proc, reason} = msg) when is_reference(ref) do
defp do_find(tasks, {:DOWN, ref, _, proc, reason} = msg) when is_reference(ref) do
find = fn %Task{ref: task_ref} -> task_ref == ref end
if Enum.find(tasks, find) do
exit({reason(reason, proc), {__MODULE__, :find, [tasks, msg]}})
end
end
def find(_tasks, _msg) do
defp do_find(_tasks, _msg) do
nil
end
@doc ~S"""
@doc """
Temporarily blocks the current process waiting for a task reply.
Returns `{:ok, reply}` if the reply is received, `nil` if
@@ -539,21 +388,6 @@ defmodule Task do
monitor's `:DOWN` message is in the message queue. If it has been
demonitored or the message already received, this function will wait
for the duration of the timeout awaiting the message.
If you intend to shut the task down if it has not responded within `timeout`
milliseconds, you should chain this together with `shutdown/1`, like so:
case Task.yield(task, timeout) || Task.shutdown(task) do
{:ok, result} ->
result
nil ->
Logger.warn "Failed to get a result in #{timeout}ms"
nil
end
That ensures that if the task completes after the `timeout` but before `shutdown/1`
has been called, you will still get the result, since `shutdown/1` is designed to
handle this case and return the result.
"""
@spec yield(t, timeout) :: {:ok, term} | {:exit, term} | nil
def yield(task, timeout \\ 5_000)
@@ -606,7 +440,7 @@ defmodule Task do
tasks =
for i <- 1..10 do
Task.async(fn ->
Process.sleep(i * 1000)
:timer.sleep(i * 1000)
i
end)
end
@@ -646,7 +480,7 @@ defmodule Task do
end
end
defp yield_many([%Task{ref: ref, owner: owner} = task | rest], timeout_ref, timeout) do
defp yield_many([%Task{ref: ref, owner: owner}=task | rest], timeout_ref, timeout) do
if owner != self() do
raise ArgumentError, invalid_owner_error(task)
end
@@ -695,9 +529,9 @@ defmodule Task do
`:shutdown` to shutdown all of its linked processes, including tasks, that
are not trapping exits without generating any log messages.
If a task's monitor has already been demonitored or received and there is not
a response waiting in the message queue this function will return
`{:exit, :noproc}` as the result or exit reason can not be determined.
This function assumes the task's monitor is still active or the monitor's
`:DOWN` message is in the message queue. If it has been demonitored, or the
message already received, this function will block forever awaiting the message.
"""
@spec shutdown(t, timeout | :brutal_kill) :: {:ok, term} | {:exit, term} | nil
def shutdown(task, shutdown \\ 5_000)
@@ -711,10 +545,9 @@ defmodule Task do
end
def shutdown(%Task{pid: pid} = task, :brutal_kill) do
mon = Process.monitor(pid)
exit(pid, :kill)
case shutdown_receive(task, mon, :brutal_kill, :infinity) do
case shutdown_receive(task, :brutal_kill, :infinity) do
{:down, proc, :noconnection} ->
exit({reason(:noconnection, proc), {__MODULE__, :shutdown, [task, :brutal_kill]}})
{:down, _, reason} ->
@@ -725,9 +558,8 @@ defmodule Task do
end
def shutdown(%Task{pid: pid} = task, timeout) do
mon = Process.monitor(pid)
exit(pid, :shutdown)
case shutdown_receive(task, mon, :shutdown, timeout) do
case shutdown_receive(task, :shutdown, timeout) do
{:down, proc, :noconnection} ->
exit({reason(:noconnection, proc), {__MODULE__, :shutdown, [task, timeout]}})
{:down, _, reason} ->
@@ -765,24 +597,18 @@ defmodule Task do
end
end
defp shutdown_receive(%{ref: ref} = task, mon, type, timeout) do
defp shutdown_receive(%{ref: ref} = task, type, timeout) do
receive do
{:DOWN, ^mon, _, _, :shutdown} when type in [:shutdown, :timeout_kill] ->
Process.demonitor(ref, [:flush])
{:DOWN, ^ref, _, _, :shutdown} when type in [:shutdown, :timeout_kill] ->
flush_reply(ref)
{:DOWN, ^mon, _, _, :killed} when type == :brutal_kill ->
Process.demonitor(ref, [:flush])
{:DOWN, ^ref, _, _, :killed} when type == :brutal_kill ->
flush_reply(ref)
{:DOWN, ^mon, _, proc, :noproc} ->
reason = flush_noproc(ref, proc, type)
flush_reply(ref) || reason
{:DOWN, ^mon, _, proc, reason} ->
Process.demonitor(ref, [:flush])
{:DOWN, ^ref, _, proc, reason} ->
flush_reply(ref) || {:down, proc, reason}
after
timeout ->
Process.exit(task.pid, :kill)
shutdown_receive(task, mon, :timeout_kill, :infinity)
shutdown_receive(task, :timeout_kill, :infinity)
end
end
@@ -794,21 +620,6 @@ defmodule Task do
end
end
defp flush_noproc(ref, proc, type) do
receive do
{:DOWN, ^ref, _, _, :shutdown} when type in [:shutdown, :timeout_kill] ->
nil
{:DOWN, ^ref, _, _, :killed} when type == :brutal_kill ->
nil
{:DOWN, ^ref, _, _, reason} ->
{:down, proc, reason}
after
0 ->
Process.demonitor(ref, [:flush])
{:down, proc, :noproc}
end
end
defp invalid_owner_error(task) do
"task #{inspect task} must be queried from the owner but was queried from #{inspect self()}"
end
+21 -366
View File
@@ -1,5 +1,6 @@
defmodule Task.Supervised do
@moduledoc false
@ref_timeout 5_000
def start(info, fun) do
@@ -10,21 +11,29 @@ defmodule Task.Supervised do
{:ok, :proc_lib.spawn_link(__MODULE__, :noreply, [info, fun])}
end
def start_link(caller, monitor, info, fun) do
{:ok, spawn_link(caller, monitor, info, fun)}
def start_link(caller, link, info, fun) do
{:ok, spawn_link(caller, link, info, fun)}
end
def spawn_link(caller, monitor \\ :nomonitor, info, fun) do
:proc_lib.spawn_link(__MODULE__, :reply, [caller, monitor, info, fun])
def spawn_link(caller, link \\ :nolink, info, fun) do
:proc_lib.spawn_link(__MODULE__, :reply, [caller, link, info, fun])
end
def reply(caller, monitor, info, mfa) do
def reply(caller, link, info, mfa) do
initial_call(mfa)
case monitor do
case link do
:link ->
try do
Process.link(caller)
catch
:error, :noproc ->
exit({:shutdown, :noproc})
end
reply(caller, nil, @ref_timeout, info, mfa)
:monitor ->
mref = Process.monitor(caller)
reply(caller, mref, @ref_timeout, info, mfa)
:nomonitor ->
:nolink ->
reply(caller, nil, :infinity, info, mfa)
end
end
@@ -96,9 +105,9 @@ defmodule Task.Supervised do
end
defp exit(_info, _mfa, _log_reason, reason)
when reason == :normal
when reason == :shutdown
when tuple_size(reason) == 2 and elem(reason, 0) == :shutdown do
when reason == :normal
when reason == :shutdown
when tuple_size(reason) == 2 and elem(reason, 0) == :shutdown do
exit(reason)
end
@@ -111,7 +120,7 @@ defmodule Task.Supervised do
'** When function == ~p~n' ++
'** arguments == ~p~n' ++
'** Reason for termination == ~n' ++
'** ~p~n', [self(), get_from(info), fun, args, get_reason(log_reason)])
'** ~p~n', [self, get_from(info), fun, args, get_reason(log_reason)])
exit(reason)
end
@@ -123,7 +132,7 @@ defmodule Task.Supervised do
defp get_running({mod, fun, args}), do: {:erlang.make_fun(mod, fun, length(args)), args}
defp get_reason({:undef, [{mod, fun, args, _info} | _] = stacktrace} = reason)
when is_atom(mod) and is_atom(fun) do
when is_atom(mod) and is_atom(fun) do
cond do
:code.is_loaded(mod) === false ->
{:"module could not be loaded", stacktrace}
@@ -139,358 +148,4 @@ defmodule Task.Supervised do
defp get_reason(reason) do
reason
end
## Stream
def stream(enumerable, acc, reducer, mfa, options, spawn) do
next = &Enumerable.reduce(enumerable, &1, fn x, acc -> {:suspend, [x | acc]} end)
max_concurrency = Keyword.get(options, :max_concurrency, System.schedulers_online)
ordered? = Keyword.get(options, :ordered, true)
timeout = Keyword.get(options, :timeout, 5000)
on_timeout = Keyword.get(options, :on_timeout, :exit)
parent = self()
{:trap_exit, trap_exit?} = Process.info(self(), :trap_exit)
# Start a process responsible for spawning processes and translating "down"
# messages. This process will trap exits if the current process is trapping
# exit, or it won't trap exits otherwise.
{monitor_pid, monitor_ref} =
Process.spawn(fn ->
stream_monitor(parent, mfa, spawn, trap_exit?, timeout)
end, [:link, :monitor])
# Now that we have the pid of the "monitor" process and the reference of the
# monitor we use to monitor such process, we can inform the monitor process
# about our reference to it.
send(monitor_pid, {parent, monitor_ref})
config = %{
reducer: reducer,
monitor_pid: monitor_pid,
monitor_ref: monitor_ref,
ordered: ordered?,
timeout: timeout,
on_timeout: on_timeout,
}
stream_reduce(acc, max_concurrency, _spawned = 0, _delivered = 0, _waiting = %{}, next, config)
end
defp stream_reduce({:halt, acc}, _max, _spawned, _delivered, _waiting, next, config) do
%{monitor_pid: monitor_pid, monitor_ref: monitor_ref, timeout: timeout} = config
stream_close(monitor_pid, monitor_ref, timeout)
is_function(next) && next.({:halt, []})
{:halted, acc}
end
defp stream_reduce({:suspend, acc}, max, spawned, delivered, waiting, next, config) do
continuation = &stream_reduce(&1, max, spawned, delivered, waiting, next, config)
{:suspended, acc, continuation}
end
# All spawned, all delivered, next is :done.
defp stream_reduce({:cont, acc}, _max, spawned, delivered, _waiting, next,
%{monitor_pid: monitor_pid, monitor_ref: monitor_ref, timeout: timeout})
when spawned == delivered and next == :done do
stream_close(monitor_pid, monitor_ref, timeout)
{:done, acc}
end
# No more tasks to spawn because max == 0 or next is :done. We wait for task
# responses or tasks going down.
defp stream_reduce({:cont, acc}, max, spawned, delivered, waiting, next, config)
when max == 0
when next == :done do
%{monitor_pid: monitor_pid, monitor_ref: monitor_ref,
timeout: timeout, on_timeout: on_timeout, ordered: ordered?} = config
receive do
# The task at position "position" replied with "value". We put the
# response in the "waiting" map and do nothing, since we'll only act on
# this response when the replying task dies (we'll notice in the :down
# message).
{{^monitor_ref, position}, reply} ->
%{^position => {pid, :running}} = waiting
waiting = Map.put(waiting, position, {pid, {:ok, reply}})
stream_reduce({:cont, acc}, max, spawned, delivered, waiting, next, config)
# The task at position "position" died for some reason. We check if it
# replied already (then the death is peaceful) or if it's still running
# (then the reply from this task will be {:exit, reason}). This message is
# sent to us by the monitor process, not by the dying task directly.
{kind, {^monitor_ref, position}, reason} when kind in [:down , :timed_out] ->
result =
case waiting do
# If the task replied, we don't care whether it went down for timeout
# or for normal reasons.
%{^position => {_, {:ok, _} = ok}} ->
ok
# If the task exited by itself before replying, we emit {:exit, reason}.
%{^position => {_, :running}} when kind == :down ->
{:exit, reason}
# If the task timed out before replying, we either exit (on_timeout: :exit)
# or emit {:exit, :timeout} (on_timeout: :kill_task) (note the task is already
# dead at this point).
%{^position => {_, :running}} when kind == :timed_out ->
if on_timeout == :exit do
stream_cleanup_inbox(monitor_pid, monitor_ref)
exit({:timeout, {__MODULE__, :stream, [timeout]}})
else
{:exit, :timeout}
end
end
if ordered? do
waiting = Map.put(waiting, position, {:done, result})
stream_deliver({:cont, acc}, max + 1, spawned, delivered, waiting, next, config)
else
pair = deliver_now(result, acc, next, config)
stream_reduce(pair, max + 1, spawned, delivered + 1, waiting, next, config)
end
# The monitor process died. We just cleanup the messages from the monitor
# process and exit.
{:DOWN, ^monitor_ref, _, ^monitor_pid, reason} ->
stream_cleanup_inbox(monitor_pid, monitor_ref)
exit({reason, {__MODULE__, :stream, [timeout]}})
end
end
defp stream_reduce({:cont, acc}, max, spawned, delivered, waiting, next, config) do
%{monitor_pid: monitor_pid, monitor_ref: monitor_ref,
timeout: timeout} = config
try do
next.({:cont, []})
catch
kind, reason ->
stacktrace = System.stacktrace
stream_close(monitor_pid, monitor_ref, timeout)
:erlang.raise(kind, reason, stacktrace)
else
{:suspended, [value], next} ->
waiting = stream_spawn(value, spawned, waiting, monitor_pid, monitor_ref, timeout)
stream_reduce({:cont, acc}, max - 1, spawned + 1, delivered, waiting, next, config)
{_, [value]} ->
waiting = stream_spawn(value, spawned, waiting, monitor_pid, monitor_ref, timeout)
stream_reduce({:cont, acc}, max - 1, spawned + 1, delivered, waiting, :done, config)
{_, []} ->
stream_reduce({:cont, acc}, max, spawned, delivered, waiting, :done, config)
end
end
defp deliver_now(reply, acc, next, config) do
%{reducer: reducer, monitor_pid: monitor_pid,
monitor_ref: monitor_ref, timeout: timeout} = config
try do
reducer.(reply, acc)
catch
kind, reason ->
stacktrace = System.stacktrace()
is_function(next) && next.({:halt, []})
stream_close(monitor_pid, monitor_ref, timeout)
:erlang.raise(kind, reason, stacktrace)
end
end
defp stream_deliver({:suspend, acc}, max, spawned, delivered, waiting, next, config) do
continuation = &stream_deliver(&1, max, spawned, delivered, waiting, next, config)
{:suspended, acc, continuation}
end
defp stream_deliver({:halt, acc}, max, spawned, delivered, waiting, next, config) do
stream_reduce({:halt, acc}, max, spawned, delivered, waiting, next, config)
end
defp stream_deliver({:cont, acc}, max, spawned, delivered, waiting, next, config) do
%{reducer: reducer, monitor_pid: monitor_pid,
monitor_ref: monitor_ref, timeout: timeout} = config
case waiting do
%{^delivered => {:done, reply}} ->
try do
reducer.(reply, acc)
catch
kind, reason ->
stacktrace = System.stacktrace
is_function(next) && next.({:halt, []})
stream_close(monitor_pid, monitor_ref, timeout)
:erlang.raise(kind, reason, stacktrace)
else
pair ->
stream_deliver(pair, max, spawned, delivered + 1, Map.delete(waiting, delivered), next, config)
end
%{} ->
stream_reduce({:cont, acc}, max, spawned, delivered, waiting, next, config)
end
end
defp stream_close(monitor_pid, monitor_ref, timeout) do
send(monitor_pid, {:stop, monitor_ref})
receive do
{:DOWN, ^monitor_ref, _, _, :normal} ->
stream_cleanup_inbox(monitor_pid, monitor_ref)
:ok
{:DOWN, ^monitor_ref, _, _, reason} ->
stream_cleanup_inbox(monitor_pid, monitor_ref)
exit({reason, {__MODULE__, :stream, [timeout]}})
end
end
defp stream_cleanup_inbox(monitor_pid, monitor_ref) do
receive do
{:EXIT, ^monitor_pid, _} -> stream_cleanup_inbox(monitor_ref)
after
0 -> stream_cleanup_inbox(monitor_ref)
end
end
defp stream_cleanup_inbox(monitor_ref) do
receive do
{{^monitor_ref, _}, _} ->
stream_cleanup_inbox(monitor_ref)
{kind, {^monitor_ref, _}, _} when kind in [:down, :timed_out] ->
stream_cleanup_inbox(monitor_ref)
after
0 ->
:ok
end
end
# This function spawns a task for the given "value", and puts the pid of this
# new task in the map of "waiting" tasks, which is returned.
defp stream_spawn(value, spawned, waiting, monitor_pid, monitor_ref, timeout) do
send(monitor_pid, {:spawn, spawned, value})
receive do
{:spawned, {^monitor_ref, ^spawned}, pid} ->
send(pid, {self(), {monitor_ref, spawned}})
Map.put(waiting, spawned, {pid, :running})
{:DOWN, ^monitor_ref, _, ^monitor_pid, reason} ->
stream_cleanup_inbox(monitor_pid, monitor_ref)
exit({reason, {__MODULE__, :stream, [timeout]}})
end
end
defp stream_monitor(parent_pid, mfa, spawn, trap_exit?, timeout) do
Process.flag(:trap_exit, trap_exit?)
parent_ref = Process.monitor(parent_pid)
# Let's wait for the parent process to tell this process the monitor ref
# it's using to monitor this process. If the parent process dies while this
# process waits, this process dies with the same reason.
receive do
{^parent_pid, monitor_ref} ->
config = %{
parent_pid: parent_pid,
parent_ref: parent_ref,
mfa: mfa,
spawn: spawn,
monitor_ref: monitor_ref,
timeout: timeout,
}
stream_monitor_loop(_running_tasks = %{}, config)
{:DOWN, ^parent_ref, _, _, reason} ->
exit(reason)
end
end
defp stream_monitor_loop(running_tasks, config) do
%{parent_pid: parent_pid, parent_ref: parent_ref, mfa: mfa,
spawn: spawn, monitor_ref: monitor_ref, timeout: timeout} = config
receive do
# The parent process is telling us to spawn a new task to process
# "value". We spawn it and notify the parent about its pid.
{:spawn, position, value} ->
{type, pid} = spawn.(parent_pid, normalize_mfa_with_arg(mfa, value))
ref = Process.monitor(pid)
# Schedule a timeout message to ourselves, unless the timeout was set to :infinity
timer_ref = case timeout do
:infinity -> nil
timeout -> Process.send_after(self(), {:timeout, {monitor_ref, ref}}, timeout)
end
send(parent_pid, {:spawned, {monitor_ref, position}, pid})
task_info = %{
position: position,
type: type,
pid: pid,
timer_ref: timer_ref,
timed_out?: false,
}
running_tasks = Map.put(running_tasks, ref, task_info)
stream_monitor_loop(running_tasks, config)
# The parent process is telling us to stop because the stream is being
# closed. In this case, we forcely kill all spawned processes and then
# exit gracefully ourselves.
{:stop, ^monitor_ref} ->
Process.flag(:trap_exit, true)
for {ref, %{pid: pid}} <- running_tasks do
Process.exit(pid, :kill)
receive do
{:DOWN, ^ref, _, _, _} -> :ok
end
end
exit(:normal)
# The parent process went down with a given reason. We kill all the
# spawned processes (that are also linked) with the same reason, and then
# exit ourself with the same reason.
{:DOWN, ^parent_ref, _, _, reason} ->
for {_ref, %{type: :link, pid: pid}} <- running_tasks do
Process.exit(pid, reason)
end
exit(reason)
# One of the spawned processes went down. We inform the parent process of
# this and keep going.
{:DOWN, ref, _, _, reason} ->
{%{position: position, timer_ref: timer_ref, timed_out?: timed_out?}, running_tasks} = Map.pop(running_tasks, ref)
if timer_ref != nil do
:ok = Process.cancel_timer(timer_ref, async: true, info: false)
end
message_kind = if(timed_out?, do: :timed_out, else: :down)
send(parent_pid, {message_kind, {monitor_ref, position}, reason})
stream_monitor_loop(running_tasks, config)
# One of the spawned processes timed out. We kill that process here
# regardless of the value of :on_timeout. We then send a message to the
# parent process informing it that a task timed out, and the parent
# process decides what to do.
{:timeout, {^monitor_ref, ref}} ->
running_tasks =
case running_tasks do
%{^ref => %{pid: pid, timed_out?: false} = task_info} ->
unlink_and_kill(pid)
Map.put(running_tasks, ref, %{task_info | timed_out?: true})
_other ->
running_tasks
end
stream_monitor_loop(running_tasks, config)
{:EXIT, _, _} ->
stream_monitor_loop(running_tasks, config)
end
end
defp unlink_and_kill(pid) do
caller = self()
ref = make_ref()
enforcer = spawn(fn ->
mon = Process.monitor(caller)
receive do
{:done, ^ref} -> :ok
{:DOWN, ^mon, _, _, _} -> Process.exit(pid, :kill)
end
end)
Process.unlink(pid)
Process.exit(pid, :kill)
send(enforcer, {:done, ref})
end
defp normalize_mfa_with_arg({mod, fun, args}, arg), do: {mod, fun, [arg | args]}
defp normalize_mfa_with_arg(fun, arg), do: {:erlang, :apply, [fun, [arg]]}
end
+31 -183
View File
@@ -3,66 +3,45 @@ defmodule Task.Supervisor do
A task supervisor.
This module defines a supervisor which can be used to dynamically
supervise tasks.
supervise tasks. Behind the scenes, this module is implemented as a
`:simple_one_for_one` supervisor where the workers are temporary
(i.e. they are not restarted after they die).
`start_link/1` can be used to start the supervisor. See the `Task`
module for more examples.
See the `Task` module for more information.
## Name registration
## Name Registration
A `Task.Supervisor` is bound to the same name registration rules as a
`GenServer`. Read more about them in the `GenServer` docs.
"""
@typedoc "Option values used by `start_link`"
@type option :: Supervisor.option |
{:restart, :supervisor.restart} |
{:shutdown, :supervisor.shutdown}
@doc false
def child_spec(arg) do
%{
id: Task.Supervivsor,
start: {Task.Supervisor, :start_link, [arg]},
type: :supervisor
}
end
@doc """
Starts a new supervisor.
The supported options are:
* `:name` - used to register a supervisor name, the supported values are
described under the `Name Registration` section in the `GenServer` module
docs;
* `:name` - used to register a supervisor name, the supported values are
described under the `Name Registration` section in the `GenServer` module
docs;
* `:restart` - the restart strategy, may be `:temporary` (the default),
`:transient` or `:permanent`. `:temporary` means the task is never
restarted, `:transient` means it is restarted if the exit is not
`:normal`, `:shutdown` or `{:shutdown, reason}`. A `:permanent` restart
strategy means it is always restarted. It defaults to `:temporary` so
tasks aren't automatically restarted when they complete nor in case of
crashes. Note the `:async` functions in this module support only `:temporary`
restarts;
* `:restart` - the restart strategy, may be `:temporary` (the default),
`:transient` or `:permanent`. Check `Supervisor.Spec` for more info.
Defaults to `:temporary` so tasks aren't automatically restarted when
they complete nor in case of crashes;
* `:shutdown` - `:brutal_kill` if the tasks must be killed directly on shutdown
or an integer indicating the timeout value, defaults to 5000 milliseconds;
* `:shutdown` - `:brutal_kill` if the tasks must be killed directly on shutdown
or an integer indicating the timeout value, defaults to 5000 milliseconds;
* `:max_restarts` and `:max_seconds` - as specified in `Supervisor`;
* `:max_restarts` and `:max_seconds` - as specified in `Supervisor.Spec.supervise/2`;
"""
@spec start_link([option]) :: Supervisor.on_start
@spec start_link(Supervisor.options) :: Supervisor.on_start
def start_link(opts \\ []) do
import Supervisor.Spec
{restart, opts} = Keyword.pop(opts, :restart, :temporary)
{shutdown, opts} = Keyword.pop(opts, :shutdown, 5000)
child = %{
id: Task.Supervised,
start: {Task.Supervised, :start_link, []},
restart: restart,
shutdown: shutdown
}
Supervisor.start_link([child], [strategy: :simple_one_for_one] ++ opts)
children = [worker(Task.Supervised, [], restart: restart, shutdown: shutdown)]
Supervisor.start_link(children, [strategy: :simple_one_for_one] ++ opts)
end
@doc """
@@ -71,12 +50,8 @@ defmodule Task.Supervisor do
The `supervisor` must be a reference as defined in `Task.Supervisor`.
The task will still be linked to the caller, see `Task.async/3` for
more information and `async_nolink/2` for a non-linked variant.
Note this function requires the task supervisor to have `:temporary`
as the `:restart` option (the default), as `async/2` keeps a direct
reference to the task which is lost if the task is restarted.
"""
@spec async(Supervisor.supervisor, (() -> any)) :: Task.t
@spec async(Supervisor.supervisor, fun) :: Task.t
def async(supervisor, fun) do
async(supervisor, :erlang, :apply, [fun, []])
end
@@ -87,14 +62,10 @@ defmodule Task.Supervisor do
The `supervisor` must be a reference as defined in `Task.Supervisor`.
The task will still be linked to the caller, see `Task.async/3` for
more information and `async_nolink/2` for a non-linked variant.
Note this function requires the task supervisor to have `:temporary`
as the `:restart` option (the default), as `async/4` keeps a direct
reference to the task which is lost if the task is restarted.
"""
@spec async(Supervisor.supervisor, module, atom, [term]) :: Task.t
def async(supervisor, module, fun, args) do
do_async(supervisor, :link, module, fun, args)
do_async(supervisor, module, fun, args, :link)
end
@doc """
@@ -104,15 +75,11 @@ defmodule Task.Supervisor do
The task won't be linked to the caller, see `Task.async/3` for
more information.
Note this function requires the task supervisor to have `:temporary`
as the `:restart` option (the default), as `async_nolink/2` keeps a
direct reference to the task which is lost if the task is restarted.
## Compatibility with OTP behaviours
If you create a task using `async_nolink` inside an OTP behaviour
like `GenServer`, you should match on the message coming from the
task inside your `c:GenServer.handle_info/2` callback.
task inside your `handle_info` callback.
The reply sent by the task will be in the format `{ref, result}`,
where `ref` is the monitor reference held by the task struct
@@ -123,7 +90,7 @@ defmodule Task.Supervisor do
with the same `ref` value that is held by the task struct. If the task
terminates normally, the reason in the `:DOWN` message will be `:normal`.
"""
@spec async_nolink(Supervisor.supervisor, (() -> any)) :: Task.t
@spec async_nolink(Supervisor.supervisor, fun) :: Task.t
def async_nolink(supervisor, fun) do
async_nolink(supervisor, :erlang, :apply, [fun, []])
end
@@ -134,120 +101,10 @@ defmodule Task.Supervisor do
The `supervisor` must be a reference as defined in `Task.Supervisor`.
The task won't be linked to the caller, see `Task.async/3` for
more information.
Note this function requires the task supervisor to have `:temporary`
as the `:restart` option (the default), as `async_nolink/4` keeps a
direct reference to the task which is lost if the task is restarted.
"""
@spec async_nolink(Supervisor.supervisor, module, atom, [term]) :: Task.t
def async_nolink(supervisor, module, fun, args) do
do_async(supervisor, :nolink, module, fun, args)
end
@doc """
Returns a stream that runs the given `module`, `function`, and `args`
concurrently on each item in `enumerable`.
Each item will be prepended to the given `args` and processed by its
own task. The tasks will be spawned under the given `supervisor` and
linked to the current process, similarly to `async/4`.
When streamed, each task will emit `{:ok, value}` upon successful
completion or `{:exit, reason}` if the caller is trapping exits.
Results are emitted in the same order as the original `enumerable`.
The level of concurrency can be controlled via the `:max_concurrency`
option and defaults to `System.schedulers_online/0`. A timeout
can also be given as an option representing the maximum amount of
time to wait without a task reply.
Note this function requires the task supervisor to have `:temporary`
as the `:restart` option (the default), as `async_stream/6` keeps a
direct reference to the task which is lost if the task is restarted.
Finally, if you find yourself trapping exits to handle exits inside
the async stream, consider using `async_stream_nolink/6` to start tasks
that are not linked to the current process.
## Options
* `:max_concurrency` - sets the maximum number of tasks to run
at the same time. Defaults to `System.schedulers_online/0`.
* `:ordered` - whether the results should be returned in the same order
as the input stream. This option is useful when you have large
streams and don't want to buffer results before they are delivered.
Defaults to `true`.
* `:timeout` - the maximum amount of time to wait (in milliseconds)
without receiving a task reply (across all running tasks).
Defaults to `5000`.
* `:on_timeout` - what do to when a task times out. The possible
values are:
* `:exit` (default) - the process that spawned the tasks exits.
* `:kill_task` - the task that timed out is killed. The value
emitted for that task is `{:exit, :timeout}`.
## Examples
Let's build a stream and then enumerate it:
stream = Task.Supervisor.async_stream(MySupervisor, collection, Mod, :expensive_fun, [])
Enum.to_list(stream)
"""
@spec async_stream(Supervisor.supervisor, Enumerable.t, module, atom, [term], keyword) ::
Enumerable.t
def async_stream(supervisor, enumerable, module, function, args, options \\ [])
when is_atom(module) and is_atom(function) and is_list(args) do
build_stream(supervisor, :link, enumerable, {module, function, args}, options)
end
@doc """
Returns a stream that runs the given function `fun` concurrently
on each item in `enumerable`.
Each item in `enumerable` is passed as argument to the given function `fun`
and processed by its own task. The tasks will be spawned under the given
`supervisor` and linked to the current process, similarly to `async/2`.
See `async_stream/6` for discussion, options, and examples.
"""
@spec async_stream(Supervisor.supervisor, Enumerable.t, (term -> term), keyword) ::
Enumerable.t
def async_stream(supervisor, enumerable, fun, options \\ []) when is_function(fun, 1) do
build_stream(supervisor, :link, enumerable, fun, options)
end
@doc """
Returns a stream that runs the given `module`, `function`, and `args`
concurrently on each item in `enumerable`.
Each item in `enumerable` will be prepended to the given `args` and processed
by its own task. The tasks will be spawned under the given `supervisor` and
will not be linked to the current process, similarly to `async_nolink/4`.
See `async_stream/6` for discussion, options, and examples.
"""
@spec async_stream_nolink(Supervisor.supervisor, Enumerable.t, module, atom, [term], keyword) ::
Enumerable.t
def async_stream_nolink(supervisor, enumerable, module, function, args, options \\ [])
when is_atom(module) and is_atom(function) and is_list(args) do
build_stream(supervisor, :nolink, enumerable, {module, function, args}, options)
end
@doc """
Returns a stream that runs the given `function` concurrently on each
item in `enumerable`.
Each item in `enumerable` is passed as argument to the given function `fun`
and processed by its own task. The tasks will be spawned under the given
`supervisor` and linked to the current process, similarly to `async_nolink/2`.
See `async_stream/6` for discussion and examples.
"""
@spec async_stream_nolink(Supervisor.supervisor, Enumerable.t, (term -> term), keyword) ::
Enumerable.t
def async_stream_nolink(supervisor, enumerable, fun, options \\ []) when is_function(fun, 1) do
build_stream(supervisor, :nolink, enumerable, fun, options)
do_async(supervisor, module, fun, args, :monitor)
end
@doc """
@@ -259,7 +116,7 @@ defmodule Task.Supervisor do
end
@doc """
Returns all children PIDs.
Returns all children pids.
"""
@spec children(Supervisor.supervisor) :: [pid]
def children(supervisor) do
@@ -274,7 +131,7 @@ defmodule Task.Supervisor do
task needs to perform side-effects (like I/O) and does not need
to report back to the caller.
"""
@spec start_child(Supervisor.supervisor, (() -> any)) :: {:ok, pid}
@spec start_child(Supervisor.supervisor, fun) :: {:ok, pid}
def start_child(supervisor, fun) do
start_child(supervisor, :erlang, :apply, [fun, []])
end
@@ -286,34 +143,25 @@ defmodule Task.Supervisor do
by the given `module`, `fun` and `args`.
"""
@spec start_child(Supervisor.supervisor, module, atom, [term]) :: {:ok, pid}
def start_child(supervisor, module, fun, args) when is_atom(fun) and is_list(args) do
Supervisor.start_child(supervisor, [get_info(self()), {module, fun, args}])
def start_child(supervisor, module, fun, args) do
Supervisor.start_child(supervisor, [get_info(self), {module, fun, args}])
end
defp get_info(self) do
{node(),
case Process.info(self, :registered_name) do
{:registered_name, []} -> self
{:registered_name, []} -> self()
{:registered_name, name} -> name
end}
end
defp do_async(supervisor, link_type, module, fun, args) do
defp do_async(supervisor, module, fun, args, link_type) do
owner = self()
args = [owner, :monitor, get_info(owner), {module, fun, args}]
args = [owner, link_type, get_info(owner), {module, fun, args}]
{:ok, pid} = Supervisor.start_child(supervisor, args)
if link_type == :link, do: Process.link(pid)
ref = Process.monitor(pid)
send pid, {owner, ref}
%Task{pid: pid, ref: ref, owner: owner}
end
defp build_stream(supervisor, link_type, enumerable, fun, options) do
&Task.Supervised.stream(enumerable, &1, &2, fun, options, fn owner, mfa ->
args = [owner, :monitor, get_info(owner), mfa]
{:ok, pid} = Supervisor.start_child(supervisor, args)
if link_type == :link, do: Process.link(pid)
{link_type, pid}
end)
end
end
+2 -43
View File
@@ -2,49 +2,8 @@ defmodule Tuple do
@moduledoc """
Functions for working with tuples.
Tuples are ordered collections of elements; tuples can contain elements of any
type, and a tuple can contain elements of different types. Curly braces can be
used to create tuples:
iex> {}
{}
iex> {1, :two, "three"}
{1, :two, "three"}
Tuples store elements contiguously in memory; this means that accessing a
tuple element by index (which can be done through the `Kernel.elem/2`
function) is a constant-time operation:
iex> tuple = {1, :two, "three"}
iex> elem(tuple, 0)
1
iex> elem(tuple, 2)
"three"
Same goes for getting the tuple size (via `Kernel.tuple_size/1`):
iex> tuple_size({})
0
iex> tuple_size({1, 2, 3})
3
Tuples being stored contiguously in memory also means that updating a tuple
(for example replacing an element with `Kernel.put_elem/3`) will make a copy
of the whole tuple.
Tuples are not meant to be used as a "collection" type (which is also
suggested by the absence of an implementation of the `Enumerable` protocol for
tuples): they're mostly meant to be used as a fixed-size container for
multiple elements. For example, tuples are often used to have functions return
"enriched" values: a common pattern is for functions to return `{:ok, value}`
for successful cases and `{:error, reason}` for unsuccessful cases. For
example, this is exactly what `File.read/1` does: it returns `{:ok, contents}`
if reading the given file is successful, or `{:error, reason}` otherwise
(e.g., `{:error, :enoent}` if the file doesn't exist).
This module provides functions to work with tuples; some more functions to
work with tuples can be found in `Kernel` (`Kernel.tuple_size/1`,
`Kernel.elem/2`, `Kernel.put_elem/3`, and others).
See also `Kernel.elem/2`, `Kernel.is_tuple/1`,
`Kernel.put_elem/3`, and `Kernel.tuple_size/1`.
"""
@doc """
+28 -52
View File
@@ -3,8 +3,8 @@ defmodule URI do
Utilities for working with URIs.
This module provides functions for working with URIs (for example, parsing
URIs or encoding query strings). The functions in this module are implemented
according to [RFC 3986](https://tools.ietf.org/html/rfc3986).
URIs or encoding query strings). For reference, most of the functions in this
module refer to [RFC 3986](https://tools.ietf.org/html/rfc3986).
"""
defstruct scheme: nil, path: nil, query: nil,
@@ -40,13 +40,13 @@ defmodule URI do
nil
"""
@spec default_port(binary) :: nil | non_neg_integer
@spec default_port(binary) :: nil | pos_integer
def default_port(scheme) when is_binary(scheme) do
:elixir_config.safe_get({:uri, scheme}, nil)
:elixir_config.get({:uri, scheme})
end
@doc """
Registers the default `port` for the given `scheme`.
Registers the default port `port` for the given `scheme`.
After this function is called, `port` will be returned by
`default_port/1` for the given scheme `scheme`. Note that this function
@@ -57,8 +57,8 @@ defmodule URI do
application's start callback in case you want to register
new URIs.
"""
@spec default_port(binary, non_neg_integer) :: :ok
def default_port(scheme, port) when is_binary(scheme) and is_integer(port) and port >= 0 do
@spec default_port(binary, pos_integer) :: :ok
def default_port(scheme, port) when is_binary(scheme) and is_integer(port) and port > 0 do
:elixir_config.put({:uri, scheme}, port)
end
@@ -157,9 +157,7 @@ defmodule URI do
nil ->
dict
{{key, value}, rest} ->
# Avoid warnings about Dict being deprecated
dict_module = Dict
decode_query_into_dict(rest, dict_module.put(dict, key, value))
decode_query_into_dict(rest, Dict.put(dict, key, value))
end
end
@@ -204,7 +202,7 @@ defmodule URI do
Checks if the character is a "reserved" character in a URI.
Reserved characters are specified in
[RFC 3986, section 2.2](https://tools.ietf.org/html/rfc3986#section-2.2).
[RFC 3986, section 2.2](http://tools.ietf.org/html/rfc3986#section-2.2).
## Examples
@@ -221,7 +219,7 @@ defmodule URI do
Checks if the character is a "unreserved" character in a URI.
Unreserved characters are specified in
[RFC 3986, section 2.3](https://tools.ietf.org/html/rfc3986#section-2.3).
[RFC 3986, section 2.3](http://tools.ietf.org/html/rfc3986#section-2.3).
## Examples
@@ -255,21 +253,14 @@ defmodule URI do
end
@doc """
Percent-escapes all characters that require escaped in a string.
Percent-escapes the given 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.
This function accepts a `predicate` function as an optional argument; if
passed, this function will be called with each character (byte) in `str` as
its argument and should return `true` if that character should not be escaped
and left as is.
See `encode_www_form` 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 `true` if the given
byte should be left as is.
## Examples
## Example
iex> URI.encode("ftp://s-ite.tld/?value=put it+й")
"ftp://s-ite.tld/?value=put%20it+%D0%B9"
@@ -352,8 +343,8 @@ defmodule URI do
unpercent(tail, <<acc::binary, ?\s>>, spaces)
end
defp unpercent(<<?%, hex1, hex2, tail::binary>>, acc, spaces) do
unpercent(tail, <<acc::binary, bsl(hex_to_dec(hex1), 4) + hex_to_dec(hex2)>>, spaces)
defp unpercent(<<?%, hex_1, hex_2, tail::binary>>, acc, spaces) do
unpercent(tail, <<acc::binary, bsl(hex_to_dec(hex_1), 4) + hex_to_dec(hex_2)>>, spaces)
end
defp unpercent(<<?%, _::binary>>, _acc, _spaces), do: throw(:malformed_uri)
@@ -375,7 +366,7 @@ defmodule URI do
`URI.parse/1` can be used to parse a wide range of URIs.
This function uses the parsing regular expression as defined
in [RFC 3986, Appendix B](https://tools.ietf.org/html/rfc3986#appendix-B).
in [RFC 3986, Appendix B](http://tools.ietf.org/html/rfc3986#appendix-B).
When a URI is given without a port, the value returned by
`URI.default_port/1` for the URI's scheme is used for the `:port` field.
@@ -409,8 +400,8 @@ defmodule URI do
def parse(%URI{} = uri), do: uri
def parse(string) when is_binary(string) do
# From https://tools.ietf.org/html/rfc3986#appendix-B
regex = Regex.recompile!(~r/^(([a-z][a-z0-9\+\-\.]*):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/i)
# From http://tools.ietf.org/html/rfc3986#appendix-B
regex = ~r/^(([a-z][a-z0-9\+\-\.]*):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/i
parts = nillify(Regex.run(regex, string))
destructure [_, _, scheme, _, authority, path, _, query, _, fragment], parts
@@ -428,8 +419,7 @@ defmodule URI do
# Split an authority into its userinfo, host and port parts.
defp split_authority(string) do
regex = Regex.recompile!(~r/(^(.*)@)?(\[[a-zA-Z0-9:.]*\]|[^:]*)(:(\d*))?/)
components = Regex.run(regex, string || "")
components = Regex.run(~r/(^(.*)@)?(\[[a-zA-Z0-9:.]*\]|[^:]*)(:(\d*))?/, string || "")
destructure [_, _, userinfo, host, _, port], nillify(components)
host = if host, do: host |> String.trim_leading("[") |> String.trim_trailing("]")
@@ -463,7 +453,7 @@ defmodule URI do
Merges two URIs.
This function merges two URIs as per
[RFC 3986, section 5.2](https://tools.ietf.org/html/rfc3986#section-5.2).
[RFC 3986, section 5.2](http://tools.ietf.org/html/rfc3986#section-5.2).
## Examples
@@ -481,10 +471,7 @@ defmodule URI do
raise ArgumentError, "you must merge onto an absolute URI"
end
def merge(_base, %URI{scheme: rel_scheme} = rel) when rel_scheme != nil do
%{rel | path: remove_dot_segments_from_path(rel.path)}
end
def merge(base, %URI{authority: authority} = rel) when authority != nil do
%{rel | scheme: base.scheme, path: remove_dot_segments_from_path(rel.path)}
rel
end
def merge(%URI{} = base, %URI{path: rel_path} = rel) when rel_path in ["", nil] do
%{base | query: rel.query || base.query, fragment: rel.fragment}
@@ -500,7 +487,7 @@ defmodule URI do
defp merge_paths(nil, rel_path),
do: merge_paths("/", rel_path)
defp merge_paths(_, "/" <> _ = rel_path),
do: remove_dot_segments_from_path(rel_path)
do: rel_path
defp merge_paths(base_path, rel_path) do
[_ | base_segments] = path_to_segments(base_path)
path_to_segments(rel_path)
@@ -509,17 +496,6 @@ defmodule URI do
|> Enum.join("/")
end
defp remove_dot_segments_from_path(nil) do
nil
end
defp remove_dot_segments_from_path(path) do
path
|> path_to_segments()
|> remove_dot_segments([])
|> Enum.join("/")
end
defp remove_dot_segments([], [head, ".." | acc]),
do: remove_dot_segments([], [head | acc])
defp remove_dot_segments([], acc),
@@ -533,7 +509,7 @@ defmodule URI do
defp remove_dot_segments([head | tail], acc),
do: remove_dot_segments(tail, [head | acc])
defp path_to_segments(path) do
def path_to_segments(path) do
[head | tail] = String.split(path, "/")
reverse_and_discard_empty(tail, [head])
end
@@ -557,7 +533,7 @@ defimpl String.Chars, for: URI do
_ -> uri
end
# Based on https://tools.ietf.org/html/rfc3986#section-5.3
# Based on http://tools.ietf.org/html/rfc3986#section-5.3
authority = extract_authority(uri)
if(scheme, do: scheme <> ":", else: "") <>
@@ -573,7 +549,7 @@ defimpl String.Chars, for: URI do
defp extract_authority(%{host: host, userinfo: userinfo, port: port}) do
# According to the grammar at
# https://tools.ietf.org/html/rfc3986#appendix-A, a "host" can have a colon
# in it only if it's an IPv6 or "IPvFuture" address, so if there's a colon
# in it only if it's an IPv6 or "IPvFuture" address), so if there's a colon
# in the host we can safely surround it with [].
if(userinfo, do: userinfo <> "@", else: "") <>
if(String.contains?(host, ":"), do: "[" <> host <> "]", else: host) <>
+148 -177
View File
@@ -24,15 +24,15 @@ defmodule Version do
## Struct
The version is represented by the `Version` struct and fields
are named according to SemVer: `:major`, `:minor`, `:patch`,
`:pre`, and `:build`.
The version is represented by the Version struct and fields
are named according to Semver: `:major`, `:minor`, `:patch`,
`:pre` and `:build`.
## Requirements
Requirements allow you to specify which versions of a given
dependency you are willing to work against. Requirements support common
operators like `>=`, `<=`, `>`, `==`, and friends that
dependency you are willing to work against. It supports common
operators like `>=`, `<=`, `>`, `==` and friends that
work as one would expect:
# Only version 2.0.0
@@ -64,10 +64,10 @@ defmodule Version do
`~> 2.0` | `>= 2.0.0 and < 3.0.0`
`~> 2.1` | `>= 2.1.0 and < 3.0.0`
When `allow_pre: false` is set, the requirement will not match a
When `allow_pre: false` is set the requirement will not match a
pre-release version unless the operand is a pre-release version.
The default is to always allow pre-releases but note that in
Hex `:allow_pre` is set to `false`. See the table below for examples.
The default is to allow always allow pre-releases but note that in
Hex `:allow_pre` is set to `false.` See the table below for examples.
Requirement | Version | `:allow_pre` | Matches
:------------- | :---------- | :----------- | :------
@@ -111,27 +111,11 @@ defmodule Version do
end
defmodule InvalidRequirementError do
defexception [:requirement]
def exception(requirement) when is_binary(requirement) do
%__MODULE__{requirement: requirement}
end
def message(%{requirement: requirement}) do
"invalid requirement: #{inspect requirement}"
end
defexception [:message]
end
defmodule InvalidVersionError do
defexception [:version]
def exception(version) when is_binary(version) do
%__MODULE__{version: version}
end
def message(%{version: version}) do
"invalid version: #{inspect version}"
end
defexception [:message]
end
@doc """
@@ -139,32 +123,32 @@ defmodule Version do
Returns `true` if `version` satisfies `requirement`, `false` otherwise.
Raises a `Version.InvalidRequirementError` exception if `requirement` is not
parsable, or a `Version.InvalidVersionError` exception if `version` is not parsable.
parsable, or `Version.InvalidVersionError` if `version` is not parsable.
If given an already parsed version and requirement this function won't
raise.
## Options
* `:allow_pre` (boolean) - when `false`, pre-release versions will not match
unless the operand is a pre-release version. See the table above
for examples. Defaults to `true`.
* `:allow_pre` - when `false` pre-release versions will not match
unless the operand is a pre-release version, see the table above
for examples (default: `true`);
## Examples
iex> Version.match?("2.0.0", "> 1.0.0")
iex> Version.match?("2.0.0", ">1.0.0")
true
iex> Version.match?("2.0.0", "== 1.0.0")
iex> Version.match?("2.0.0", "==1.0.0")
false
iex> Version.match?("foo", "== 1.0.0")
** (Version.InvalidVersionError) invalid version: "foo"
iex> Version.match?("foo", "==1.0.0")
** (Version.InvalidVersionError) foo
iex> Version.match?("2.0.0", "== == 1.0.0")
** (Version.InvalidRequirementError) invalid requirement: "== == 1.0.0"
iex> Version.match?("2.0.0", "== ==1.0.0")
** (Version.InvalidRequirementError) == ==1.0.0
"""
@spec match?(version, requirement, keyword) :: boolean
@spec match?(version, requirement, Keyword.t) :: boolean
def match?(version, requirement, opts \\ [])
def match?(version, requirement, opts) when is_binary(requirement) do
@@ -172,7 +156,7 @@ defmodule Version do
{:ok, requirement} ->
match?(version, requirement, opts)
:error ->
raise InvalidRequirementError, requirement
raise InvalidRequirementError, message: requirement
end
end
@@ -188,39 +172,23 @@ defmodule Version do
end
@doc """
Compares two versions.
Compares two versions. Returns `:gt` if first version is greater than
the second and `:lt` for vice versa. If the two versions are equal `:eq`
is returned
Returns `:gt` if the first version is greater than the second one, and `:lt`
for vice versa. If the two versions are equal, `:eq` is returned.
Pre-releases are strictly less than their corresponding release versions.
Patch segments are compared lexicographically if they are alphanumeric, and
numerically otherwise.
Build segments are ignored: if two versions differ only in their build segment
they are considered to be equal.
Raises a `Version.InvalidVersionError` exception if any of the two given
versions are not parsable. If given an already parsed version this function
won't raise.
Raises a `Version.InvalidVersionError` exception if `version` is not parsable.
If given an already parsed version this function won't raise.
## Examples
iex> Version.compare("2.0.1-alpha1", "2.0.0")
:gt
iex> Version.compare("1.0.0-beta", "1.0.0-rc1")
:lt
iex> Version.compare("1.0.0-10", "1.0.0-2")
:gt
iex> Version.compare("2.0.1+build0", "2.0.1")
:eq
iex> Version.compare("invalid", "2.0.1")
** (Version.InvalidVersionError) invalid version: "invalid"
** (Version.InvalidVersionError) invalid
"""
@spec compare(version, version) :: :gt | :eq | :lt
@@ -241,7 +209,7 @@ defmodule Version do
end
@doc """
Parses a version string into a `Version` struct.
Parses a version string into a `Version`.
## Examples
@@ -256,10 +224,9 @@ defmodule Version do
@spec parse(String.t) :: {:ok, t} | :error
def parse(string) when is_binary(string) do
case Version.Parser.parse_version(string) do
{:ok, {major, minor, patch, pre, build_parts}} ->
build = if build_parts == [], do: nil, else: Enum.join(build_parts, "")
{:ok, {major, minor, patch, pre}} ->
version = %Version{major: major, minor: minor, patch: patch,
pre: pre, build: build}
pre: pre, build: get_build(string)}
{:ok, version}
:error ->
:error
@@ -277,24 +244,24 @@ defmodule Version do
#Version<2.0.1-alpha1>
iex> Version.parse!("2.0-alpha1")
** (Version.InvalidVersionError) invalid version: "2.0-alpha1"
** (Version.InvalidVersionError) 2.0-alpha1
"""
@spec parse!(String.t) :: t | no_return
def parse!(string) when is_binary(string) do
case parse(string) do
{:ok, version} -> version
:error -> raise InvalidVersionError, string
:error -> raise InvalidVersionError, message: string
end
end
@doc """
Parses a version requirement string into a `Version.Requirement` struct.
Parses a version requirement string into a `Version.Requirement`.
## Examples
iex> {:ok, requirement} = Version.parse_requirement("== 2.0.1")
iex> requirement
iex> {:ok, req} = Version.parse_requirement("== 2.0.1")
iex> req
#Version.Requirement<== 2.0.1>
iex> Version.parse_requirement("== == 2.0.1")
@@ -315,7 +282,7 @@ defmodule Version do
Compiles a requirement to its internal representation with
`:ets.match_spec_compile/1` for faster matching.
The internal representation is opaque and cannot be converted to external
The internal representation is opaque and can not be converted to external
term format and then back again without losing its properties (meaning it
can not be sent to a process on another node and still remain a valid
compiled match_spec, nor can it be stored on disk).
@@ -331,59 +298,97 @@ defmodule Version do
defp to_matchable(string, allow_pre?) do
case Version.Parser.parse_version(string) do
{:ok, {major, minor, patch, pre, _build_parts}} ->
{:ok, {major, minor, patch, pre}} ->
{major, minor, patch, pre, allow_pre?}
:error ->
raise InvalidVersionError, string
raise InvalidVersionError, message: string
end
end
defp get_build(string) do
case Regex.run(~r/\+([^\s]+)$/, string) do
nil ->
nil
[_, build] ->
build
end
end
defmodule Parser.DSL do
@moduledoc false
defmacro deflexer(match, do: body) when is_binary(match) do
quote do
def lexer(unquote(match) <> rest, acc) do
lexer(rest, [unquote(body) | acc])
end
end
end
defmacro deflexer(acc, do: body) do
quote do
def lexer("", unquote(acc)) do
unquote(body)
end
end
end
defmacro deflexer(char, acc, do: body) do
quote do
def lexer(<<unquote(char)::utf8, rest::binary>>, unquote(acc)) do
unquote(char) = <<unquote(char)::utf8>>
lexer(rest, unquote(body))
end
end
end
end
defmodule Parser do
@moduledoc false
import Parser.DSL
operators = [
{">=", :>=},
{"<=", :<=},
{"~>", :~>},
{">", :>},
{"<", :<},
{"==", :==},
{"!=", :!=},
{"!", :!=},
{" or ", :||},
{" and ", :&&},
]
for {string_op, atom_op} <- operators do
def lexer(unquote(string_op) <> rest, acc) do
lexer(rest, [unquote(atom_op) | acc])
deflexer ">=", do: :>=
deflexer "<=", do: :<=
deflexer "~>", do: :~>
deflexer ">", do: :>
deflexer "<", do: :<
deflexer "==", do: :==
deflexer "!=", do: :!=
deflexer "!", do: :!=
deflexer " or ", do: :||
deflexer " and ", do: :&&
deflexer " ", do: :' '
deflexer x, [] do
[x, :'==']
end
deflexer x, [h | acc] do
cond do
is_binary h ->
[h <> x | acc]
h in [:||, :&&] ->
[x, :==, h | acc]
true ->
[x, h | acc]
end
end
def lexer(" " <> rest, acc) do
lexer(rest, acc)
deflexer acc do
Enum.filter(Enum.reverse(acc), &(&1 != :' '))
end
def lexer(<<char::utf8, rest::binary>>, []) do
lexer(rest, [<<char::utf8>>, :==])
end
def lexer(<<char::utf8, body::binary>>, [head | acc]) do
acc =
case head do
head when is_binary(head) ->
[<<head::binary, char::utf8>> | acc]
head when head in [:||, :&&] ->
[<<char::utf8>>, :==, head | acc]
_other ->
[<<char::utf8>>, head | acc]
end
lexer(body, acc)
end
def lexer("", acc) do
Enum.reverse(acc)
end
@version_regex ~r/^
(\d+) # major
(?:\.(\d+))? # minor
(?:\.(\d+))? # patch
(?:\-([\d\w\.\-]+))? # pre
(?:\+([\d\w\.\-]+))? # build
$/x
@spec parse_requirement(String.t) :: {:ok, term} | :error
def parse_requirement(source) do
@@ -391,87 +396,53 @@ defmodule Version do
to_matchspec(lexed)
end
defp nillify(""), do: nil
defp nillify(o), do: o
@spec parse_version(String.t) :: {:ok, Version.matchable} | :error
def parse_version(string, approximate? \\ false) when is_binary(string) do
destructure [version_with_pre, build], String.split(string, "+", parts: 2)
destructure [version, pre], String.split(version_with_pre, "-", parts: 2)
destructure [major, minor, patch, next], String.split(version, ".")
if parsed = Regex.run(@version_regex, string) do
destructure [_, major, minor, patch, pre], parsed
patch = nillify(patch)
pre = nillify(pre)
with nil <- next,
{:ok, major} <- require_digits(major),
{:ok, minor} <- require_digits(minor),
{:ok, patch} <- maybe_patch(patch, approximate?),
{:ok, pre_parts} <- optional_dot_separated(pre),
{:ok, pre_parts} <- convert_parts_to_integer(pre_parts, []),
{:ok, build_parts} <- optional_dot_separated(build) do
{:ok, {major, minor, patch, pre_parts, build_parts}}
else
_other -> :error
end
end
if is_nil(minor) or (is_nil(patch) and not approximate?) do
:error
else
major = String.to_integer(major)
minor = String.to_integer(minor)
patch = patch && String.to_integer(patch)
defp require_digits(nil), do: :error
defp require_digits(string) do
if leading_zero?(string), do: :error, else: parse_digits(string, "")
end
defp leading_zero?(<<?0, _, _::binary>>), do: true
defp leading_zero?(_), do: false
defp parse_digits(<<char, rest::binary>>, acc) when char in ?0..?9,
do: parse_digits(rest, <<acc::binary, char>>)
defp parse_digits(<<>>, acc) when byte_size(acc) > 0,
do: {:ok, String.to_integer(acc)}
defp parse_digits(_, _acc),
do: :error
defp maybe_patch(patch, approximate?)
defp maybe_patch(nil, true), do: {:ok, nil}
defp maybe_patch(patch, _), do: require_digits(patch)
defp optional_dot_separated(nil), do: {:ok, []}
defp optional_dot_separated(string) do
parts = String.split(string, ".")
if Enum.all?(parts, &(&1 != "" and valid_identifier?(&1))) do
{:ok, parts}
case parse_pre(pre) do
{:ok, pre} ->
{:ok, {major, minor, patch, pre}}
:error ->
:error
end
end
else
:error
end
end
defp convert_parts_to_integer([part | rest], acc) do
case parse_digits(part, "") do
{:ok, integer} ->
if leading_zero?(part) do
:error
else
convert_parts_to_integer(rest, [integer | acc])
end
:error ->
convert_parts_to_integer(rest, [part | acc])
defp parse_pre(nil), do: {:ok, []}
defp parse_pre(pre), do: parse_pre(String.split(pre, "."), [])
defp parse_pre([piece | t], acc) do
cond do
piece =~ ~r/^(0|[1-9][0-9]*)$/ ->
parse_pre(t, [String.to_integer(piece) | acc])
piece =~ ~r/^[0-9]*$/ ->
:error
true ->
parse_pre(t, [piece | acc])
end
end
defp convert_parts_to_integer([], acc) do
defp parse_pre([], acc) do
{:ok, Enum.reverse(acc)}
end
defp valid_identifier?(<<char, rest::binary>>)
when char in ?0..?9
when char in ?a..?z
when char in ?A..?Z
when char == ?- do
valid_identifier?(rest)
end
defp valid_identifier?(<<>>) do
true
end
defp valid_identifier?(_other) do
false
end
defp valid_requirement?([]), do: false
defp valid_requirement?([a | next]), do: valid_requirement?(a, next)
@@ -589,7 +560,7 @@ defmodule Version do
defp parse_condition(version, approximate? \\ false) do
case parse_version(version, approximate?) do
{:ok, {major, minor, patch, pre, _build}} -> {major, minor, patch, pre}
{:ok, version} -> version
:error -> throw :invalid_matchspec
end
end

Some files were not shown because too many files have changed in this diff Show More