Skip to content

[#725] New practice exercise pov #752

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 13 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
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
22 changes: 22 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2357,6 +2357,28 @@
"macros"
],
"difficulty": 8
},
{
"slug": "pov",
"name": "POV",
"uuid": "abc93451-451d-4497-b590-68cbf82e0a20",
"prerequisites": [
"pattern-matching",
"case",
"if",
"errors",
"recursion",
"enum",
"protocols",
"nil",
"structs",
"multiple-clause-functions"
],
"practices": [
"recursion",
"pattern-matching"
],
"difficulty": 9
Copy link
Member

Choose a reason for hiding this comment

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

This is exciting 🤩 looks like this will be our most difficult exercise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have not done many of the difficulty 8, but it definitely is one step above Zipper, which is 8.
Forth is a 10 though!

Copy link
Contributor

Choose a reason for hiding this comment

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

I think difficult 9 is okay, we don't have a firm definition what each number means anymore, so it's just to give a general feel of the complexity

}
],
"foregone": [
Expand Down
38 changes: 38 additions & 0 deletions exercises/practice/pov/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Description

Reparent a graph on a selected node.

This exercise is all about re-orientating a graph to see things from a different
point of view. For example family trees are usually presented from the
ancestor's perspective:

```text
+------0------+
| | |
+-1-+ +-2-+ +-3-+
| | | | | |
4 5 6 7 8 9
```

But the same information can be presented from the perspective of any other node
in the graph, by pulling it up to the root and dragging its relationships along
with it. So the same graph from 6's perspective would look like:

```text
6
|
+-----2-----+
| |
7 +-----0-----+
| |
+-1-+ +-3-+
| | | |
4 5 8 9
```

This lets us more simply describe the paths between two nodes. So for example
the path from 6-9 (which in the first graph goes up to the root and then down to
a different leaf node) can be seen to follow the path 6-2-0-3-9

This exercise involves taking an input graph and re-orientating it from the point
of view of one of the nodes.
4 changes: 4 additions & 0 deletions exercises/practice/pov/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
23 changes: 23 additions & 0 deletions exercises/practice/pov/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"blurb": "Reparent a graph on a selected node.",
"authors": [
"jiegillet"
],
"contributors": [
"neenjaw",
"angelikatyborska"
],
"files": {
"solution": [
"lib/pov.ex"
],
"test": [
"test/pov_test.exs"
],
"example": [
".meta/example.ex"
]
},
"source": "Adaptation of exercise from 4clojure",
"source_url": "https://www.4clojure.com/"
}
133 changes: 133 additions & 0 deletions exercises/practice/pov/.meta/example.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
defmodule Pov do
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this exercise is basically Zipper + more functions, should see if we are being congruent with our approaches between the two exercises

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Coincidentally, I just reached the Zipper exercise in my Elixir track. I'll go solve it real quick before looking into the repo ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I've solved it now. The approach in Zipper is quite different from other exercises that I've seen so far since it provides a complete module for binary trees. It's not bad, should I model this one after Zipper?

Copy link
Member

Choose a reason for hiding this comment

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

I think we can start with your current implementation like it is now, and then later, if people complain that the exercise is too difficult, we could add hints in the form of a module with a data structure that we suggest using.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, challenging exercises are not as much of an issue for practice exercises

# Structs and types

@typedoc """
A tree, which is made of a node with several branches
"""
@type tree :: {any, [tree]}

defmodule Crumb do
defstruct [:parent, left_siblings: [], right_siblings: []]
@type t :: %Crumb{parent: any, left_siblings: [Pov.tree()], right_siblings: [Pov.tree()]}
end

defmodule Zipper do
defstruct [:focus, genealogy: []]
@type t :: %Zipper{focus: Pov.tree(), genealogy: [Crumb.t()]}
end

# Core functions

@doc """
Reparent a tree on a selected node.
"""
@spec from_pov(tree :: tree, node :: any) :: tree | {:error, atom}
def from_pov(tree, node) do
case tree |> zip |> search(node) do
{:ok, zipper} -> reparent(zipper)
_ -> {:error, :nonexistent_target}
end
end

@doc """
Finds a path between two nodes
"""
@spec path_between(tree :: tree, from :: any, to :: any) :: [any] | {:error, atom}
def path_between(tree, from, to) do
case tree |> zip |> search(from) do
{:ok, zipper} ->
case zipper |> reparent |> zip |> search(to) do
{:ok, zipper_path} -> get_path(zipper_path)
_ -> {:error, :nonexistent_destination}
end

_ ->
{:error, :nonexistent_source}
end
end

def search(%Zipper{focus: {node, _children}} = zipper, node), do: {:ok, zipper}

def search(%Zipper{} = zipper, node) do
case zipper |> down |> search(node) do
{:ok, z} -> {:ok, z}
_ -> zipper |> right |> search(node)
end
end

def search(nil, _node), do: nil

def reparent(%Zipper{focus: tree, genealogy: []}), do: tree

def reparent(%Zipper{
focus: {node, children},
genealogy: [
%Crumb{parent: parent, left_siblings: left, right_siblings: right} | grandparent
]
}) do
{node, [reparent(%Zipper{focus: {parent, left ++ right}, genealogy: grandparent}) | children]}
end

def get_path(%Zipper{focus: {node, _children}, genealogy: genealogy}) do
parents = Enum.map(genealogy, fn %Crumb{parent: parent} -> parent end)

Enum.reverse([node | parents])
end

# Zipper navigation
# up and left are not actually required for this problem

def zip(tree), do: %Zipper{focus: tree}

def down(%Zipper{focus: {value, [child | children]}, genealogy: genealogy}) do
%Zipper{
focus: child,
genealogy: [%Crumb{parent: value, right_siblings: children} | genealogy]
}
end

def down(_zipper), do: nil

def up(%Zipper{
focus: tree,
genealogy: [
%Crumb{parent: parent, left_siblings: left, right_siblings: right} | grandparents
]
}) do
%Zipper{focus: {parent, left ++ [tree | right]}, genealogy: grandparents}
end

def up(_zipper), do: nil

def left(%Zipper{
focus: tree,
genealogy: [
%Crumb{left_siblings: [left | lefties], right_siblings: right} = crumb | grandparents
]
}) do
%Zipper{
focus: left,
genealogy: [
%Crumb{crumb | left_siblings: lefties, right_siblings: [tree | right]} | grandparents
]
}
end

def left(_zipper), do: nil

def right(%Zipper{
focus: tree,
genealogy: [
%Crumb{left_siblings: left, right_siblings: [right | righties]} = crumb | grandparents
]
}) do
%Zipper{
focus: right,
genealogy: [
%Crumb{crumb | left_siblings: [tree | left], right_siblings: righties} | grandparents
]
}
end

def right(_zipper), do: nil
end
48 changes: 48 additions & 0 deletions exercises/practice/pov/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# This is an auto-generated file. Regular comments will be removed when this
# file is regenerated. Regenerating will not touch any manually added keys,
# so comments can be added in a "comment" key.

[1b3cd134-49ad-4a7d-8376-7087b7e70792]
description = "Results in the same tree if the input tree is a singleton"

[0778c745-0636-40de-9edd-25a8f40426f6]
description = "Can reroot a tree with a parent and one sibling"

[fdfdef0a-4472-4248-8bcf-19cf33f9c06e]
description = "Can reroot a tree with a parent and many siblings"

[cbcf52db-8667-43d8-a766-5d80cb41b4bb]
description = "Can reroot a tree with new root deeply nested in tree"

[e27fa4fa-648d-44cd-90af-d64a13d95e06]
description = "Moves children of the new root to same level as former parent"

[09236c7f-7c83-42cc-87a1-25afa60454a3]
description = "Can reroot a complex tree with cousins"

[f41d5eeb-8973-448f-a3b0-cc1e019a4193]
description = "Errors if target does not exist in a singleton tree"

[9dc0a8b3-df02-4267-9a41-693b6aff75e7]
description = "Errors if target does not exist in a large tree"

[02d1f1d9-428d-4395-b026-2db35ffa8f0a]
description = "Can find path to parent"

[d0002674-fcfb-4cdc-9efa-bfc54e3c31b5]
description = "Can find path to sibling"

[c9877cd1-0a69-40d4-b362-725763a5c38f]
description = "Can find path to cousin"

[9fb17a82-2c14-4261-baa3-2f3f234ffa03]
description = "Can find path not involving root"

[5124ed49-7845-46ad-bc32-97d5ac7451b2]
description = "Can find path from nodes other than x"

[f52a183c-25cc-4c87-9fc9-0e7f81a5725c]
description = "Errors if destination does not exist"

[f4fe18b9-b4a2-4bd5-a694-e179155c2149]
description = "Errors if source does not exist"
20 changes: 20 additions & 0 deletions exercises/practice/pov/lib/pov.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Pov do
@typedoc """
A tree, which is made of a node with several branches
"""
@type tree :: {any, [tree]}
Copy link
Member

Choose a reason for hiding this comment

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

Great simple choice 👍


@doc """
Reparent a tree on a selected node.
"""
@spec from_pov(tree :: tree, node :: any) :: tree | {:error, atom}
def from_pov(tree, node) do
end

@doc """
Finds a path between two nodes
"""
@spec path_between(tree :: tree, from :: any, to :: any) :: [any] | {:error, atom}
def path_between(tree, from, to) do
end
end
28 changes: 28 additions & 0 deletions exercises/practice/pov/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule Pov.MixProject do
use Mix.Project

def project do
[
app: :pov,
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
Loading