Skip to content

Make doctests from code texts for case/2 #14423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 12, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 50 additions & 52 deletions lib/elixir/lib/kernel/special_forms.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1909,33 +1909,31 @@ defmodule Kernel.SpecialForms do

## Examples

case File.read(file) do
{:ok, contents} when is_binary(contents) ->
String.split(contents, "\n")

{:error, _reason} ->
Logger.warning "could not find #{file}, assuming empty..."
[]
end
iex> file = "no_file.txt"
iex> case File.read(file) do
...> {:ok, contents} when is_binary(contents) ->
...> String.split(contents, "\n")
...> {:error, _reason} ->
...> "Can't read the #{file} file"
...> end
"Can't read the no_file.txt file"
Comment on lines -1912 to +1919
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only concern here is that the previous version, although not a runnable doctest, felt like a more idiomatic example (log and fallback on a sensible default) vs returning a string.

Copy link
Contributor Author

@dmitrykleymenov dmitrykleymenov Apr 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be rewritten into something like:

iex> require Logger
iex> file = "no_file.txt"
iex> case File.read(file) do
...>   {:ok, contents} when is_binary(contents) ->
...>     String.split(contents, "\n")
...>   {:error, _reason} ->
...>     Logger.warning "Can't read the #{file} file, assuming empty..."
...>     []  
...> end
[]

?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all code in docs is or can be evaluated (side effects, complicated setup etc) (see this), and I think it is fine.

Non-doctests code examples aren't prefixed by iex>.
You can see many examples in the File module.

Copy link
Contributor Author

@dmitrykleymenov dmitrykleymenov Apr 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but doctests instead of plain code texts provide assurance(for reader) of typos and other errors absence. Also doctests don't have implicit parts(as you mentioned: side effects, internal setup and other) and much more clear and easy to understand. In case when we can't avoid side effects(as in File) it's ok, but here, i'm sure, it's possible to find a good(ideomatic) pure functional example of usage, which can be doctested.
In other words, when I read a doctest i'm 100% sure it works, and that helps.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the arguments in favor of doctests and I agree they are nice when possible for pure functions.

For conditionals like case/if/cond..., hard-coding the condition to have a runnable doctest might also make the example less realistic and it only demonstrates one branch, but it seems we are not consistent across these.

i'm sure, it's possible to find a good(ideomatic) pure functional example of usage, which can be doctested.

Sounds good! How about some parsing like Date.from_iso8601/1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, i'll do a pr


In the example above, we match the result of `File.read/1`
against each clause "head" and execute the clause "body"
corresponding to the first clause that matches.
corresponding to the first clause that matches. In our case
there is no file, so `File.read/1` returns `{:error, :enoent}`
and the second clause is matched.

If no clause matches, an error is raised. For this reason,
it may be necessary to add a final catch-all clause (like `_`)
which will always match.

x = 10

case x do
0 ->
"This clause won't match"

_ ->
"This clause would match any value (x = #{x})"
end
#=> "This clause would match any value (x = 10)"
iex> x = 10
iex> case x do
...> 0 -> "This clause won't match"
...> _ -> "This clause would match any value (x = #{x})"
...> end
"This clause would match any value (x = 10)"

If you find yourself nesting `case` expressions inside
`case` expressions, consider using `with/1`.
Expand All @@ -1944,54 +1942,54 @@ defmodule Kernel.SpecialForms do

Note that variables bound in a clause do not leak to the outer context:

case data do
{:ok, value} -> value
:error -> nil
end
iex> case {:ok, 7} do
...> {:ok, value} -> value
...> :error -> nil
...> end

value
#=> unbound variable value
...> value
** (CompileError) undefined variable "value"

Variables in the outer context cannot be overridden either:

value = 7

case lucky? do
false -> value = 13
true -> true
end

value
#=> 7
iex> value = 7
iex> case 3 > 5 do
...> false ->
...> value = 3
...> value + 2
...> true ->
...> 3
...> end
iex> value
7

In the example above, `value` is going to be `7` regardless of the value of
`lucky?`. The variable `value` bound in the clause and the variable `value`
bound in the outer context are two entirely separate variables.
In the example above, `value` is going to be `7` regardless of
which clause matched. The variable `value` bound in the clause
and the variable `value` bound in the outer context are two
entirely separate variables.

If you want to pattern match against an existing variable,
you need to use the `^/1` operator:

x = 1

case 10 do
^x -> "Won't match"
_ -> "Will match"
end
#=> "Will match"
iex> x = 1
iex> case 10 do
...> ^x -> "Won't match"
...> _ -> "Will match"
...> end
"Will match"

## Using guards to match against multiple values

While it is not possible to match against multiple patterns in a single
clause, it's possible to match against multiple values by using guards:

case data do
value when value in [:one, :two] ->
"#{value} has been matched"

:three ->
"three has been matched"
end

iex> case :two do
...> value when value in [:one, :two] ->
...> "#{value} has been matched"
...> :three ->
...> "three has been matched"
...> end
"two has been matched"
"""
defmacro case(condition, clauses), do: error!([condition, clauses])

Expand Down
Loading