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

Use Semigroup constraint for LAM instead of Monoid #308

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

dwincort
Copy link

As discussed in #307, this eases the Monoid restriction labeled adjacency maps to Semigroup.

Copy link
Author

@dwincort dwincort left a comment

Choose a reason for hiding this comment

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

One function that still has a Monoid constraint is edgeLabel. I think, really, this should have the type edgeLabel :: (Semigroup e, Ord a) => a -> a -> AdjacencyMap e a -> Maybe e, where Nothing represents the lack of an edge. However, I didn't want to change the interface to the module, so I just left it alone. Let me know if you think I should change it.

I think I've updated most of the comments that mention zero or Monoid from Labelled.AdjacencyMap, but I haven't really added any new documentation (outside of the comment on trimZeroes.

@@ -155,7 +155,7 @@ instance Dioid e => Graph (LG.Graph e a) where
overlay = LG.overlay
connect = LG.connect one

instance (Dioid e, Eq e, Ord a) => Graph (LAM.AdjacencyMap e a) where
instance (Dioid e, Ord a) => Graph (LAM.AdjacencyMap e a) where
Copy link
Author

Choose a reason for hiding this comment

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

This is a little frustrating but probably acceptable. Technically, LAM.AdjacencyMap seems like it could be a Graph so long as it has the multiplicative monoid even if it doesn't have the additive one (that is, it has <.> and one but doesn't have zero). Thus, the Dioid e constraint is too strong. However, there's no nice way to represent this, so it seems like we should just require the full Dioid e constraint.

@@ -118,7 +118,7 @@ instance (Eq e, Monoid e, Ord a) => T.ToGraph (Graph e a) where
-- subgraphs.
-- Extract the adjacency map of a graph.
toAdjacencyMap :: (Eq e, Monoid e, Ord a) => Graph e a -> AM.AdjacencyMap e a
toAdjacencyMap = foldg AM.empty AM.vertex AM.connect
toAdjacencyMap = foldg AM.empty AM.vertex (\e x y -> AM.trimZeroes $ AM.connect e x y)
Copy link
Author

Choose a reason for hiding this comment

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

I haven't used this module much, but I'm pretty sure this is the right place to make a minimal change that will appropriately trim zero out of the adjacency maps produced from this module.

-- performance reasons when a monoidal edge type is used and 'zero' edges may
-- have slipped into the underlying representation.
trimZeroes :: (Eq e, Monoid e) => AdjacencyMap e a -> AdjacencyMap e a
trimZeroes (AM x) = AM $ Map.map (Map.filter (/= zero)) x
Copy link
Author

Choose a reason for hiding this comment

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

Exposing this seems like an abstraction leak. However, keeping it hidden could cause users to suffer from performance penalties if their edge type has zero.

Copy link
Owner

Choose a reason for hiding this comment

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

Given that the implementation doesn't care much about zeroes, should we provide a more general function like filterEdges?

Copy link
Author

Choose a reason for hiding this comment

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

I want to propose a radical alternative. Instead of exposing trimZeros (or filterEdges), we make the monoid explicit and this function implicit. I feel like Haskell's type classes are not quite equipped to deal with this, but I can think of a work around. We could define:

data AdjacencyMap e a = AM {
    -- | The /adjacency map/ of an edge-labelled graph: each vertex is
    -- associated with a map from its direct successors to the corresponding
    -- edge labels.
    adjacencyMap :: Map a (Map a e),
    zeroEdge :: Maybe e
  } deriving (Eq, Generic, NFData)

Then, we could have two constructors, one that takes a Semigroup constraint and sets zeroEdge = Nothing and the other that takes a Monoid constraint and sets it to mempty. We'd add back in all the uses of filterZeros, but they'd be guarded by whether zeroEdge was Nothing or not.

@dwincort
Copy link
Author

Oh, also, I left consistent :: (Ord a, Monoid e, Eq e) => AdjacencyMap e a -> Bool with a Monoid constraint. It feels like it could be useful for AdjacencyMaps where e is a Monoid.

--
-- @
-- 'edgeLabel' x y $ overlay ('edge' e x y) ('edge' 'zero' x y) == e
-- 'edgeLabel' x y $ overlay ('edge' e x y) ('edge' f x y) == e '<+>' f
-- 'edgeLabel' x y $ overlay ('edge' e x y) empty == e
Copy link
Owner

Choose a reason for hiding this comment

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

Overlaying with empty doesn't seem to make much sense here. I think I would actually prefer to keep this comment talking about <+> and zero, for symmetry with the one below, saying something like "Assuming that 'zero' exists...".

Copy link
Owner

Choose a reason for hiding this comment

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

Also, if you're changing examples here, you should update them in the testsuite too.

Copy link
Owner

@snowleopard snowleopard left a comment

Choose a reason for hiding this comment

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

Thanks! Overall, the PR looks good but I left a couple of comments.

-- defined in the top-level module "Algebra.Graph.AdjacencyMap", where @False@
-- and @True@ denote the lack of and the existence of an unlabelled edge,
-- respectively.
-- For example, 'AdjacencyMap' @()@ @a@ is isomorphic to unlabelled graphs
Copy link
Owner

Choose a reason for hiding this comment

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

I don't like that this module becomes so different from Algebra.Graph.Labelled but not sure what to do about this. Something doesn't feel right, still.

Copy link
Author

Choose a reason for hiding this comment

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

I agree. Something still feels off.

I think the weirdness comes down to the fact that the edge type, really, is a monoid, even if it's only declared as a semigroup. This is because any semigroup can be trivially lifted to a monoid by wrapping it with Maybe, or, more generally, by defining a new, separate empty value and properly updating the binary operator to treat it appropriately. In the case of these adjacency maps, we're doing exactly this: we're treating the absence of an edge as mempty, thus forming an ad hoc monoid.

So, I think the reason the above text feels off is that, morally, we're constructing AdjacencyMap (Maybe ()) a as isomorphic to unlabelled graphs, but we're totally hiding the Maybe part as an implementation detail. (To be clear, this "hiding Maybe as an implementation detail" is the whole reason I proposed this change in the first place.) This should probably be called out in this comment.

@dwincort
Copy link
Author

FYI, the project I was working on that used this got scrapped, so while I'm still happy to help make it better and/or brainstorm ideas, I wouldn't even be a user if it got merged. In other words, I won't be offended if you want to drop this PR entirely.

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.

None yet

2 participants