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

Make Graph datatype abstract, remove awkward instances #95

Open
snowleopard opened this issue Jul 18, 2018 · 14 comments
Open

Make Graph datatype abstract, remove awkward instances #95

snowleopard opened this issue Jul 18, 2018 · 14 comments

Comments

@snowleopard
Copy link
Owner

We currently expose the implementation of the Graph datatype:

data Graph a = Empty
             | Vertex a
             | Overlay (Graph a) (Graph a)
             | Connect (Graph a) (Graph a)
             deriving (Foldable, Functor, Show, Traversable)

This is problematic:

  • Users can observe different graph structures even though underlying graphs are equal according to the laws of the algebra, e.g. Empty == Overlay Empty Empty.

  • Other representations may be more efficient (e.g. see Replacing Algebra.Graph by Maybe (Algebra.Graph.NonEmpty) #90) but we can't experiment with them without potentially breaking user code.

While doing this, we should probably dump awkward instance such as Foldable, since one can also use it to reveal internal structure, e.g. toList [1] == [1] but toList [1 + 1] == [1, 1]. We have an equivalent of the Foldable type class tailored for graphs, which we call ToGraph, which relies on the user to provide correct folding functions to avoid such issues.

@nobrakal
Copy link
Contributor

So I think the right way to go is using pattern synonyms.

It allows to "hide" the real constructor, but still allows pattern matching (even if it can be really bad in performances).
For examples,

So I think it can cover almost all possible representations :)

While doing this, we should probably dump awkward instance such as Foldable

I agree that this instance is awkward, but sadly, for example, vertexList require an Ord instance on the vertices where toList does not require any.
Also losing Fodable implies losing Traversable which can be useful to someone.
Maybe we can move these instances in another module, so they do not get imported with Algebra.Graph and someone with the need of such instances can have them, but he will have to do another import, with a lot of warnings in the documentation.

@snowleopard
Copy link
Owner Author

So I think the right way to go is using pattern synonyms.

The whole point of hiding constructors is to avoid leaking any information about the structure of the graph: by providing pattern synonyms you will allow the user to distinguish x vs x + x unless you plan to always keep graphs in some normal form (which may be expensive). So I think we don't need pattern synonyms -- we'll just hide the constructors, forever :)

vertexList require an Ord instance on the vertices where toList does not require any

That's true, but I don't think it is possible to implement vertexList without Ord and without revealing the structure of the graph.

Note that we could add unorderedVertexList for internal use which would be equivalent to toList:

unorderedVertexList :: Graph a -> [a]
unorderedVertexList = foldg [] pure (++) (++)

Also losing Foldable implies losing Traversable which can be useful to someone.

Are you sure they are genuinely useful? My impression is that pretty much any use of them is a bug waiting to happen.

Maybe we can move these instances in another module

This will lead to orphan instances, I would prefer to avoid that.

@nobrakal
Copy link
Contributor

... we'll just hide the constructors, forever :)

Ah I forgot about impotence ! But you are right, goodbye poor little constructors ^^

Note that we could add unorderedVertexList for internal use which would be equivalent to toList...

Yes, that is true.

...My impression is that pretty much any use of them is a bug waiting to happen.

Certainly ^^. Moreover, everything is definable for anyone using foldg, right ? So one can define an orphan instance of Foldable and Traversable if it really need it.

For sure, all of this will need some explanations somewhere !

Note that we will also have to change Algebra.Graph.NonEmpty for coherence, I think.

@snowleopard
Copy link
Owner Author

Moreover, everything is definable for anyone using foldg, right ?

I think everything from the Foldable class is implementable using foldg. But perhaps we could also add a safe equivalent of traverse?

Note that we will also have to change Algebra.Graph.NonEmpty for coherence, I think.

Yes, you are right.

@nobrakal
Copy link
Contributor

But perhaps we could also add a safe equivalent of traverse?

Is

traverse f = foldg (pure Empty) (fmap Vertex . f) (liftA2 Overlay) (liftA2 Connect)

suitable ?

But if by safe you meant something that doesn't show the inner representation, I don't know if it is possible.

Once you have traverse, you can have foldMap (see foldMapDefault)

@snowleopard
Copy link
Owner Author

snowleopard commented Jul 19, 2018

@nobrakal Indeed, users will be able to implement traverse on their own using foldg as you described.

But if by safe you meant something that doesn't show the inner representation, I don't know if it is possible.

By "safe" I meant the user is supposed to follow the laws specified in the foldg method:

https://github.com/snowleopard/alga/blob/master/src/Algebra/Graph/ToGraph.hs#L54-L57

The method 'foldg' is used for generalised graph folding. It collapses
a given value by applying the provided graph construction primitives. The
order of arguments is: empty, vertex, overlay and connect, and it is
assumed that the arguments satisfy the axioms of the graph algebra.

In case of your traverse, we have overlay = liftA2 Overlay and require that overlay x x = x (plus all other graph laws). If the user violates this requirement, it will be possible to observe the inner graph representation, which might lead to a bug, but it's not our fault -- we give a gun to users, but they should aim it at their leg and shoot by themselves :)

@nobrakal
Copy link
Contributor

@snowleopard So is there anything else to do here ?

About Foldable it now seems to me that with mentioning the possibility of defining such instance, and its risks somewhere, it is reasonable to remove it of the API.

@snowleopard
Copy link
Owner Author

@nobrakal Yes, let's start by removing problematic instances. And then the next step will be to make Graph abstract. Feel free to send a PR if you like to try this!

@nobrakal
Copy link
Contributor

Yes, let's start by removing problematic instances.

Just realized that we will have a problem with with Algebra.Graph.HigherKinded.Class, which is based on the Foldable instance... In this module, we need:

  • null
  • elem
  • foldr
  • toList

We could require foldg to implement locally all of this, or even just a foldr function, to redefine all of these needed functions. But I am not sure it is the right way to follow :)

@snowleopard
Copy link
Owner Author

@nobrakal Perhaps, we should just drop these functions from the API. I think all of them will be available via the ToGraph typeclass if need be.

snowleopard pushed a commit that referenced this issue Oct 6, 2018
See #95.

* Remove `Foldable` and `Traversable` instances, and associated comments.
* Drop `isEmpty`, `hasVertex`, `vertexCount`, `vertexList`, `vertexSet`, `vertexIntSet` and `box` from `Algebra.Graph.HigherKinded.Class` because of the removal of the `Foldable` superclass; `mesh` and `torus` were rewritten without `box`.
* Add a comment explaining why the `Foldable` instance is problematic.
@LaurentRDC
Copy link

Are you sure they (Traversable instance) are genuinely useful? My impression is that pretty much any use of them is a bug waiting to happen.

I am currently modeling electrical networks using graphs from the containers package, and I use traverse everywhere! I am currently refactoring to use algebraic-graphs instead (specifically, Algebra.Graph.Labelled).

I don't understand why traverse on algebraic graphs is a bug waiting to happen. Foldable, sure, because it exposes internal structure. But isn't traversing the graph structure without changing it, inherently safe?

What is it about this definition of traverse that can lead to bugs?

@LaurentRDC
Copy link

LaurentRDC commented Jan 30, 2024

Actually I was bit by unexpected behavior relating to Foldable and Traversable instances for graphs.

Consider this code :

import Algebra.Graph.Labelled

graph :: Graph (Capacity Int) Char
graph = edges [(0, 'a', 'b'), (0, 'b', 'c'), (0, 'c', 'a')]

If Graph had a Foldable instance, what would expect for length graph? I would expect 3 -- there are three nodes, 'a', 'b', and 'c'.
If Graph had a Traversable instance, what would expect for traverse print graph? I would expect to see 'a', 'b', and 'c' each printed once.

The previous instances for Foldable and Traversable did not follow the behavior I expected. I ended up defining a graph datatype like so instead:

type NodeID = Int

data Network e a 
    = MkNetwork { networkTopology :: Graph (Capacity e) NodeID
                , networkNodeMap  :: Bimap NodeID a
                }

where Bimap is a bijective map (unique keys AND unique values) like the one provided by this package.

Using a data structure like this -- separating the graph nodes from the connection topology -- allows you to write intuitive Foldable and traverse where nodes are visited only once.

Hopefully this can help others

@snowleopard
Copy link
Owner Author

@LaurentRDC Yes, both Foldable and Traversable make it possible to observe the inner structure of algebraic graph expressions in a way that violates the laws of the algebra. I don't know of a good solution at the moment.

P.S. I'm curious about your application (modeling electrical networks). Where can I read more about it?

@LaurentRDC
Copy link

LaurentRDC commented Jan 30, 2024

P.S. I'm curious about your application (modeling electrical networks). Where can I read more about it?

This is not public work yet. Hopefully I'll be able to share details in the coming months. I'll try to remember to ping back then.

Edit: we have published a small library to help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants