Skip to content
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

Add a function to return the list of edges in a graph #17

Merged
merged 9 commits into from
Jul 30, 2022
Merged

Add a function to return the list of edges in a graph #17

merged 9 commits into from
Jul 30, 2022

Conversation

MaybeJustJames
Copy link
Contributor

@MaybeJustJames MaybeJustJames commented Feb 10, 2022

Adds an edges function and a corresponding Edge type.

Reasoning for the change:
I had implemented this badly in my own project. edges functionality is also necessary to implement an Eq instance for Graph if that should be considered (RE #1).


Checklist:

  • Added the change to the changelog's "Unreleased" section with a reference to this PR (e.g. "- Made a change (#0000)")
  • Linked any existing issues or proposals that this pull request should close
  • Updated or added relevant documentation
  • Added a test for the contribution (if applicable)

@MaybeJustJames MaybeJustJames changed the title Add a function the return the list of edges in a graph Add a function to return the list of edges in a graph Feb 11, 2022
@JordanMartinez
Copy link
Contributor

I think this shouldn't add a dependency on tailrec and that the topo-sort implementation could be reused for this.

@MaybeJustJames
Copy link
Contributor Author

@JordanMartinez that was significantly easier than I expected. I also exposed the Edge constructor which breaks the convention (#18 exposes a toMap rather than the Graph constructor). Shall I expose an edgeToTuple?

src/Data/Graph.purs Outdated Show resolved Hide resolved
Graph (M.fromFoldable (map (\k ->
Tuple k (Tuple (label k) (L.fromFoldable (edges k)))) ks))
Tuple k (Tuple (label k) (L.fromFoldable (theEdges k)))) ks))
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you revert these two changes since they're not relevant to this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These 2 changes are to avoid shadowing the edges name

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah! Makes sense. Ok.

Comment on lines 86 to 103
-- | List all edges in a graph
edges :: forall k v. Graph k v -> List (Edge k)
edges (Graph g) = go initialState
where
go :: EdgesState k v -> List (Edge k)
go { unvisited: Nil, result: res } = res
go { unvisited: (src /\ (_ /\ dests)) : ns, result: res } =
go
{ unvisited: ns
, result: map (Edge <<< (src /\ _)) dests <> res
}

initialState :: EdgesState k v
initialState =
{ unvisited: M.toUnfoldableUnordered g
, result: Nil
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure this is slow for a few reasons.

First, you're using List and you have at least two traversals over dests:

  1. map (Edge <<< (src /\ _)) dests: 1 traversal to create a new version of the list
  2. (map _ dests) <> res: 1 traversal appending the result of map _ dests to res. See https://github.com/JordanMartinez/purescript-jordans-reference/blob/latestRelease/21-Hello-World/05-Application-Structure/src/03-Free/01-What-Is-The-Free-Monad/04-The-Remorseless-Free-Monad.md#the-direction-matters for context.

A faster solution would to fold over dests and cons the src and destination onto res in one fold:

result: foldl (\acc dest -> Edge (src /\ dest) : acc) res dests

Second, for initialState, why convert the Map to a List in the first place via M.toUnfoldableUnordered? That traverses the map once and then you traverse the map a second time in go via the unvisited field. Since Graph is just a newtype around Map and Map has an instance for FoldableWithIndex, couldn't this same key-value information be obtained with foldlWithIndex using a single traversal rather than two as it currently does?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wow! I did not notice that at all. Thank you for taking the time to explain this all to me so clearly :)

As you mentioned I've replaced the custom function with foldlWithIndex (which traverses the Map once) and a foldl for each list of edge endpoints. So I cannot see any redundant traversals now.

@@ -42,6 +45,9 @@ instance traversableGraph :: Traversable (Graph k) where
traverse f (Graph m) = Graph <$> (traverse (\(v /\ ks) -> (_ /\ ks) <$> (f v)) m)
sequence = traverse identity

-- | An edge within a `Graph` referencing endpoint keys
newtype Edge k = Edge (Tuple k k)
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than defining an edge, it may be more helpful to define a newtype indicating which value inside the Tuple is the source and which is the destination. For example:

-newtype Edge k = Edge (Tuple k k)
+Tuple (Source k) (Destination k)

I'm not sure what the best API would be here.

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've implemented this. I don't think it's any less ergonomic and, as you say, prevents mixing up source and destination. What do you think? Is this what you meant?

Copy link
Contributor

@JordanMartinez JordanMartinez left a comment

Choose a reason for hiding this comment

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

LGTM besides the changelog issue.

@@ -19,7 +21,6 @@ Breaking changes:

New features:
- Added `Foldable` and `Traversable` instances for `Graph` (#16 by @MaybeJustJames)
- Added `toMap` to unwrap `Graph` (#18)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you put this back into the correct spot in the changelog?

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 moved this because it hasn't been released. Would you prefer a separate PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, my bad. No, let's just fix it here.

@MaybeJustJames
Copy link
Contributor Author

Can this be merged?

@JordanMartinez
Copy link
Contributor

@garyb Can this get an approval?

@garyb
Copy link
Member

garyb commented Jul 23, 2022

Another option here might be to use type Edge k = { start :: k, end :: k } (or source/destination terms if preferred)?

If we don't want to go that route, then I'd suggest including Eq/Ord/Show instances for the newtypes at least.

@MaybeJustJames
Copy link
Contributor Author

Another option here might be to use type Edge k = { start :: k, end :: k }

I've just pushed this. It still has the advantage of not mixing up source and destination without needing Eq, Ord, Show instances.

@JordanMartinez
Copy link
Contributor

🏓 @garyb

Copy link
Member

@garyb garyb left a comment

Choose a reason for hiding this comment

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

👍 Thanks for making the change!

@garyb garyb merged commit c0a9fe4 into purescript:master Jul 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants