Closed seibert closed 4 years ago
Quick note: dispatching based on literal could be useful if this literal affects the output types. For example (which is kinda ugly, but it gets the point across),
@metagraph.concrete_algorithm('community.louvian')
def nx_louvain_iterations(graph: NetworkXGraphType, output: Literal('list')) -> List[NetworkXGraphType]:
...
@metagraph.concrete_algorithm('community.louvian')
def nx_louvain(graph: NetworkXGraphType, output: Literal('final')) -> NetworkXGraphType:
...
to capture discussion from meeting: if we need to get fancy with computed return type, we can make the return type have callable which returns a type given the argument signature
I'll just leave this example here...
class AbstractType:
pass
class ConcreteType:
def __init_subclass__(cls, *, abstract=None):
if abstract is None:
raise TypeError("missing abstract. Here's how you do it...")
elif not isinstance(abstract, type) or not issubclass(abstract, AbstractType):
raise TypeError('blah blah blah')
cls.abstract = abstract
class MyAbstractType(AbstractType):
pass
class MyConcreteType(ConcreteType, abstract=MyAbstractType):
pass
class OopsIForgotAbstract(ConcreteType): # raises
pass
__init_subclass__
is also new to me, but I'm glad there is a less wonky way to validate subclasses than some metaclass magic
Closing this issue as we've addressed these topics in #5.
Here's a go at describing how to unify the ideas in both #2 and #3. My assumptions are that we need to specify:
Graph
,DenseArray
, etc) and Concrete types (NumPyArray
, etc)NumPyArray
) and actual data objects / classes (numpy.ndarray
)weight_name
) needed to allow the external object to be fully convertable between other concrete types.There are still a bunch of aesthetic/functional choices here, so here's my proposed choices with some explanation:
Type classes are distict from data objects:
typing
module declaresList
to describe list)IncidenceMatrix(transposed=False)
asks for a particular property, whereas all properties not listed (as inIncidenceMatrix
orIncidenceMatrix()
) are implicitly "any".For now, let's use actual the type classes in plugins, rather than strings. If this creates some circular import issues, we can optionally allow strings to be used as substitutes, similar to the convention with other Python type signatures.
Python inheritance is not used across the abstract and concrete type classes divide. Inheritance may be used between abstract types classes where needed.
Python inheritance is not used between concrete type classes and the data objects they describe. This is because those objects may be defined in codebases we cannot modify.
Python type signatures are used to describe both abstract and concrete algorithms for metagraph.
Types in those signatures which are instances of the metagraph abstract or concrete types will participate in the automatic translation and dispatch mechanism. Types which are basic python types (like
int
,str
ortuple
) represent "scalars" which will be passed by value without conversion.Worked example
Let's use
Graph
andWeightedGraph
to see how this works.Abstract types
Note that abstract types are basically just a class and a docstring, with inheritance showing how things might be related to each other.
Wrapper classes
An instance of
networkx.DiGraph
meets our requirement forGraph
but notWeightedGraph
. For a weighted graph, we will need to define an extra wrapper class to carry the attribute name of the weight.Concrete types
Now we need some types to describe the NetworkX instances above. Let's assume our base
ConcreteType
looks like this:The type for the NetworkX graph is:
And the weighed graph is:
Translator
A translator is a function that takes a value of one concrete type and maps it to a value of another concrete type (optionally with the desired type properties asserted). A translator might look like this:
For simplicity of dispatch, a translator must be able to handle all properties of both the source and destination concrete type. The decorator is used to add any additional methods or attributes to the functions that the system will find useful. Note that the decorator does not record this function in any global registry (see below).
Note that if a concrete type has properties, it is necessary to define a "self-translator", which is used translate the value into one with the required properties:
The
@metagraph.translator
decorator turns the function into a callable object with additional properties:src_type
: ConcreteType class of sourcedst_type
: ConcreteType class of destinationAbstract Algorithm
Abstract Algorithms are just Python functions without implementations that have a type signature that includes Abstract Types. For example, the Louvain community detection might look like this:
As with the translators, the decorator is used to add useful methods and attributes to the function, as we will see below.
Concrete Algorithm
Concrete algorithms look like the abstract algorithm, but use concrete types:
Note that this decorator does not record the
nx_louvain
method in a registry hidden inside of the abstractlouvain
algorithm. Instead it converts the function into a callable class with attributes like:nx_louvain.abstract_algorithm
: Reference back to thelouvain
object.nx_louvain.check_args(*args, **kwargs)
: Check if argument list matches function signature.If we want to define a concrete algorithm that only accepts values with a particular property (allowed properties are enumerated in the concrete type), we can do that this way:
This requires the input
graph
to have both the property offoo=True
andbar=4
, and asserts that the return value has propertyfoo=True
, but nothing else.Registration
For both testing purposes, as well as creation of special contexts, we will want to encapsulate the state associated with the registry of types, translators and algorithms. We call this state a
Resolver
, and it is responsible for:There will be an implicit, global
Resolver
created by metagraph when imported that is populated by all of the plugins in the environment. Empty resolvers can also be created and populated manually.The
Resolver
class will have methods like this:As a convenience, the resolver can also dynamically generate the algorithm namespace below it. Ex: