Skip to content

Bootstrap practice exercises #774

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 7 commits into from
Jun 26, 2021

Conversation

jiegillet
Copy link
Contributor

This script will generate all files (minus config.json and tests.toml) for a practice exercise. Of course, the tests need a bunch of manual input, but usually at the level of search and replace, so it's quite valuable.

I tried to be a little bit clever about some things (for example @SPEC generation for the lib file) but I hope that contributors understand that they should not trust these guesses too much. Is it better to make them obviously wrong?
Please test that script by running elixir bin/boostrap_practice_exercise.exs darts (or whichever exercise) and inspect the generated files. I'm sure there is room for improvement.

@github-actions
Copy link
Contributor

Thank you for contributing to exercism/elixir 💜 🎉. This is an automated PR comment 🤖 for the maintainers of this repository that helps with the PR review process. You can safely ignore it and wait for a maintainer to review your changes.

Based on the files changed in this PR, it would be good to pay attention to the following details when reviewing the PR:

  • General steps
    • 🏆 Does this PR need to receive a label with a reputation modifier (reputation/contributed_code/{minor,major})? (A medium reputation amount is awarded by default, see docs)

Automated comment created by PR Commenter 🤖.

Copy link
Member

@angelikatyborska angelikatyborska left a comment

Choose a reason for hiding this comment

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

I run it on a few existing exercises and left some (hopefully realistic) improvement ideas based on the output.

This is excellent 🎉. I guess with this script you'll churn through all the remaining "new practice exercise" issues in no time. I have to create more 😅.

Comment on lines 34 to 44
"""
@doc \"\"\"
insert function description here
\"\"\"
@spec #{property}(#{Enum.map_join(variables, ", ", fn
{var, nil} -> "#{var} :: String.t()"
{var, sub_vars} -> "#{var} :: %{#{Enum.map_join(sub_vars, " : String.t(), ", fn v -> v end)} : String.t()}"
end)}) :: {:ok, String.t()} | {:error, String.t()}
def #{property}(#{Enum.map_join(variables, ", ", fn {var, _} -> var end)}) do
end
"""
Copy link
Member

Choose a reason for hiding this comment

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

Is it better to make them obviously wrong?

Maybe it's better to somehow mark them as requiring manual changes? Maybe something like this? (and similar in other places that also have most-likely-wrong defaults)

Suggested change
"""
@doc \"\"\"
insert function description here
\"\"\"
@spec #{property}(#{Enum.map_join(variables, ", ", fn
{var, nil} -> "#{var} :: String.t()"
{var, sub_vars} -> "#{var} :: %{#{Enum.map_join(sub_vars, " : String.t(), ", fn v -> v end)} : String.t()}"
end)}) :: {:ok, String.t()} | {:error, String.t()}
def #{property}(#{Enum.map_join(variables, ", ", fn {var, _} -> var end)}) do
end
"""
"""
@doc \"\"\"
TODO: insert function description here
\"\"\"
@spec #{property}(#{Enum.map_join(variables, ", ", fn
{var, nil} -> "#{var} :: TODO.t()"
{var, sub_vars} -> "#{var} :: %{#{Enum.map_join(sub_vars, " : TODO.t(), ", fn v -> v end)} : TODO.t()}"
end)}) :: TODO.t()
def #{property}(#{Enum.map_join(variables, ", ", fn {var, _} -> var end)}) do
end
"""

test \"#{description}\" do
#{Generate.print_comments(c, ["description", "property", "input", "expected", "uuid"])}
#{print_input(input)}
output = #{module}.#{Generate.to_snake_case(property)}(#{print_variables(input)})
Copy link
Member

Choose a reason for hiding this comment

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

It would be good if changing to snake case happened as soon as possible, right after fetching and decoding the JSON, and not when printing the test case. For example, you forgot to use it when printing the stub 😉 so for an exercise like allergies (https://github.com/exercism/problem-specifications/blob/e2779fc84ff07cb5bd8ff89f38e78af6d17b5056/exercises/allergies/canonical-data.json#L14), you get a stub like this:

def allergicTo(item, score) do
end

but a function call in tests like this:

output = Allergies.allergic_to(item, score)

It's also necessary to change properties to snake case, for example: https://github.com/exercism/problem-specifications/blob/daf620d47ed905409564dec5fa9610664e294bde/exercises/all-your-base/canonical-data.json#L20-L22

Comment on lines 73 to 74
def print_expected(%{"error" => err}), do: "{:error, #{inspect(err)}}"
def print_expected(expected), do: "{:ok, #{inspect(expected)}}"
Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible (and not too difficult) to make this more intelligent, and only use {:ok, value} if any error case exists in the problem specification for this property, and otherwise just use the plain value? There are a lot of exercises without any error handling.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done :)

]
"""

Mix.Generator.create_file("exercises/practice/#{exercise}/.formatter.exs", format)
Copy link
Member

Choose a reason for hiding this comment

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

I had no idea about this module 😍 awesome

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is quite nice. And actually it wouldn't be much more work to make this script into a Mix.Task. It would be kind of cool: mix ex.practice.gen darts.

@angelikatyborska angelikatyborska requested a review from neenjaw June 22, 2021 15:45
@neenjaw
Copy link
Contributor

neenjaw commented Jun 22, 2021

I'd like to look over this one before merge, but might need to wait a couple days if that's okay. I have all next week off, so should be able to catch up on this stuff. :)


path = "exercises/practice/#{exercise}/test/#{exercise_path}_test.exs"
Mix.Generator.create_file(path, test_file)
System.cmd("mix", ["format", path])
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you know if there is a way to call mix format from within a .exs script without having to use System.cmd? I searched for it, but couldn't find it.
Although maybe it's best to run it this way so that it includes the format in .formatter.exs.

Copy link
Member

Choose a reason for hiding this comment

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

Every mix task is just a module with a run function, so it should be possible to run Mix.Tasks.Format.run([]). It should read .formatter.exs from the current directory anyway.

mix format documentation: https://hexdocs.pm/mix/master/Mix.Tasks.Format.html
and implementation: https://github.com/elixir-lang/elixir/blob/2a4312412be39303af08ccd0cef81b5bbcd55eac/lib/mix/lib/mix/tasks/format.ex#L1

@jiegillet
Copy link
Contributor Author

I guess with this script you'll churn through all the remaining "new practice exercise" issues in no time. I have to create more 😅.

I compiled a list of all the exercises we don't have yet (separating the ones with and without tests), do you want me to share it in an issue? Or do you prefer individual ones?

Comment on lines 9 to 13
def to_snake_case(lower_camel_case) do
lower_camel_case
|> String.split(~r/(?=[A-Z\d])/)
|> Enum.map_join("_", &String.downcase/1)
end
Copy link
Contributor

Choose a reason for hiding this comment

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

We can use Macro.underscore for this if we are using it just for identifiers

Comment on lines +166 to +194
defmodule #{module}.MixProject do
use Mix.Project

def project do
[
app: :#{exercise_snake_case},
version: "0.1.0",
# elixir: "~> 1.8",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor: if we can't run mix format on this file, can we indent here 2 spaces for this binding?

Copy link
Contributor Author

@jiegillet jiegillet Jun 26, 2021

Choose a reason for hiding this comment

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

At the end we run mix format on everything, it should take care of this one too..
I'm not sure what you mean, I ran mix format on the file.
Do you want to indent the triple quoted block? mix format puts it back there.

Comment on lines +200 to +202
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true)
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor, same indent as mentioned above

Copy link
Member

Choose a reason for hiding this comment

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

Here the indentation looks good already 🤔 meaning: level 0, there's no indentation.

Copy link
Contributor

@neenjaw neenjaw left a comment

Choose a reason for hiding this comment

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

Left a few minor comments, cool stuff!

@angelikatyborska
Copy link
Member

I compiled a list of all the exercises we don't have yet (separating the ones with and without tests), do you want me to share it in an issue? Or do you prefer individual ones?

There is a dashboard for this already: https://tracks.exercism.io/elixir/main/unimplemented

Sadly it doesn't mention which exercises don't have tests defined, but there's only a few left unimplemented so we can manage.

Normally only maintainers deal with exercise selection, but since you're working faster than I can review or write new issues, I want to share with you my thoughts on implementing those missing exercises:

  1. I don't want any of the exercises that don't have canonical-data.json (error handling, hangman, ledger, lens-person, paasio, robot-name, tree building) because that puts on me and Tim the responsibility to figure out which tests cases are necessary for the story, to think of the corner cases etc., it just sounds like too much work 😅
  2. I don't want any of the exercises that are ridiculously easy in a high level programming language (reverse string, maybe micro blog). That might not apply to square root because I think square root could forbid using the standard library and demand a manual implementation.
  3. I don't want high scores because it will conflict with a similar concept exercise that we already have

The rest can be implemented and your PRs with them will be very welcome if you still have the time and energy 🙂

@angelikatyborska
Copy link
Member

I'm going to make a call that all the feedback has been considered and applied, and now it's merge time 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
x:size/large Large amount of work
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants