Denomination is an experimental (proof of concept) implementation of Genus [1] in DLang.
Support for (non-default) models is limited and its API is very clunky.
Constraints can be defined as follows:
constraint("Eq")
.args("T")
.methods(
"equals".on("T").args("T").returns("bool"));
Constraints can inherit method constraints from other constraints as so:
constraint("Comparable")
.args("T")
.extends("Eq", "T")
.methods(
"compareTo".on("T").args("T").returns("int"));
Constraints can impose the existence of static methods like so:
constraint("Group")
.args("G")
.methods(
"identity".on("G").returns("G").statik,
"plus".on("G").args("G").returns("G"),
"inverse".on("G").returns("G"));
Constraints can be multi-parameteric:
constraint("GraphLike")
.args("G", "V", "E", "VId", "EId")
.methods(
"outgoingEdges".on("V").returns("EId[]"),
"incomingEdges".on("V").returns("EId[]"),
"source".on("E").returns("VId"),
"sink".on("E").returns("VId"),
"edge".on("G").args("EId").returns("E"),
"vertex".on("G").args("VId").returns("V")
));
Once a constraint is defined, it can be declared by mixing it in using the
Constraint
mixin-template like so:
mixin Constraint!(
constraint("Eq")
.args("T")
.methods(
"equals".on("T").args("T").returns("T"));
Employing constraints can be done using D's native support for constraints on
templated function/methods/templates. The two templates to use are Where
and
CheckWhere
, the former will halt compilation with useful error messages if
the constraint is not satisfied, the latter will just return false.
Here's one cool example:
// if you try to make a set with a type that doesn't implement `equals` then a
// compilation error is produced
class Set(T) if (Where!(Eq, T)) {
private T[] elements;
this() {
elements = [];
}
void add(T member) {
static if (CheckWhere!(Comp, T)) { // here we make the decision to add in order
pragma(msg, "Will use the sorted version for type ", T);
addSorted(member);
} else {
pragma(msg, "Will use naive version for type ", T);
foreach (e; elements) if (e.equals(member)) return;
elements ~= member;
}
}
// if additionally the type implements `compareTo` we will store the
// elements in a sorted fashion
private void addSorted()(T member) if (Where!(Comp, T)) {
if (elements.length == 0) {
elements ~= member;
return;
}
size_t index(size_t start, size_t end) {
if (start >= end) return end;
size_t i = (start + end) / 2;
T mid = elements[i];
int diff = member.compareTo(mid);
if (diff == 0) return -1; // no repeats in a set
else if (diff < 0) return index(start, i);
else return index(i+1, end);
}
size_t i = index(0, elements.length);
if (i >= 0) elements = elements[0..i] ~ [member] ~ elements[i..$];
}
const(T[]) members() @property {
return elements;
}
}
/**
When compiling the following messages are produced:
Will use naive version for type Point2D
Will use the sorted version for type Point
*/
unittest {
import std.stdio;
auto set1 = new Set!Point2D();
set1.add(Point2D(0,1));
set1.add(Point2D(1,0));
writeln(set1.members); // [const(Point2D)(0, 1), const(Point2D)(1, 0)]
auto set2 = new Set!Point();
set2.add(Point(3));
set2.add(Point(4));
set2.add(Point(10));
set2.add(Point(1));
writeln(set2.members); // [const(Point)(1), const(Point)(3), const(Point)(4), const(Point)(10)]
}
To demonstrate the error messages this is the program we will try
import genusdsl;
import helpers;
mixin Constraint!(
constraint("Eq")
.args("T")
.methods(
"equals".on("T").args("T").returns("bool")));
mixin Constraint!(
constraint("Group")
.args("G")
.extends("Eq", "G")
.methods(
"identity".on("G").returns("G").statik,
"plus".on("G").args("G").returns("G"),
"inverse".on("G").returns("G")));
bool checkIdentityProperties(T)(T elem = T.identity) if (Where!(Group, T)) {
T id = T.identity;
return id.equals(id.plus(id))
&& id.equals(id.inverse)
&& id.plus(elem).equals(elem) && elem.plus(id).equals(elem);
}
struct MyData {
// implementation will be here...
}
void main() {
import std.stdio;
writefln("Check passed = %s", checkIdentityProperties!MyData);
}
struct MyData {}
genusdsl.d(69): Error: static assert: "Constraint Group requires MyData to implement the method identity"
struct MyData {
bool identity(int data) { return true; }
}
genusdsl.d(37): Error: static assert: "Constraint Group(G) requires that identity be static"
struct MyData {
static bool identity(int data) { return true; }
}
genusdsl.d(59): Error: static assert: "Expected return type of identity to be MyData instead found bool"
struct MyData {
static MyData identity(int data) { return MyData(); }
}
genusdsl.d(49): Error: static assert: "Argument mismatch between Group(G)'s identity and MyData's: expected () instead found (int)"
struct MyData {
static MyData identity() { return MyData(); }
MyData plus(MyData d) { return this; }
MyData inverse() { return this; }
}
genusdsl.d(69): Error: static assert: "Constraint Eq (from Group) requires MyData to implement the method equals"
mixin Model!(
"StringCaseInsensitiveEq", Tuple!(),
Tuple!(Eq!MyStr),
Tuple!(),
MethodImpl!("equals", q{
bool equals(MyStr other) {
return this.contents.toLower == other.contents.toLower;
}
}));
mixin Model!(
"ArrayListDeepCloneable", Tuple!(Generic!"E"),
Tuple!(Cloneable!(ArrayList!(Generic!"E"), MyStr)),
Tuple!(Cloneable!(Generic!"E", MyStr), Eq!MyStr),
MethodImpl!("clone", q{
ArrayList!E clone() {
auto result = new ArrayList!E();
foreach (m; parent.contents) result.add(m.clone);
return result;
}
}));
mixin Model!(
"DualGraph", Tuple!(Generic!"V", Generic!"E", Generic!"VId", Generic!"EId"),
Tuple!(GraphLike!(Generic!"V",Generic!"E",Generic!"VId",Generic!"EId")),
Tuple!(GraphLike!(Generic!"V",Generic!"E",Generic!"VId",Generic!"EId")),
MethodImpl!("src", q{ override VId src() { return parent.target(); } }),
MethodImpl!("target", q{ override VId target() { return parent.src(); } }),
MethodImpl!("outgoing", q{ override EId[] outgoing() { return parent.incoming(); } }),
MethodImpl!("incoming", q{ override EId[] incoming() { return parent.outgoing(); } }));
The arguments of the Model
mixin are:
- The name of the model as a string
- The generic arguments of the model
- The constraints that the model is implementing
- The conditions (i.e. the
where
clauses) that must be satisfied on the model - The methods: each method has a name, then its implementation (as a string)
By default the model is always the default model, to use a model you must use
the UseModel
template that introduces (or overrides) the value that implements
the model.
If the constraints of the model depend directly on a generic, eg
Eq!(Generic!"T")
as opposed to Eq!(ArrayList!(Generic!"T"))
, then you must
provide explicitly the types to replace the generics.
Hopefully these examples are illustrative:
ArrayList!E doClone(E)(ArrayList!E lst) {
mixin UseModel!("lst", lst, ArrayListDeepCloneable!());
return lst.clone(); // lst now satisfies ArrayListDeepCloneable
}
void useBothGraphAndItsDual(V,E,VId,EId)(V vertex, E edge)
if (Where!(GraphLike,V,E,VId,EId)) {
writefln("The incoming and outgoing of `vertex` are: %s and %s",
vertex.incoming(), vertex.outgoing());
writefln("The src and target of `edge` are: %s and %s",
edge.src(), edge.target());
// dualVertex is the vertex described by the `V` generic in the `DualGraph` model
mixin UseModel!("dualVertex", vertex, DualGraph!(V, E, VId, EId), "V");
// dualEdge is the edge described by the `E` generic in the `DualGraph` model
mixin UseModel!("dualEdge", edge, DualGraph!(V, E, VId, EId), "E");
writefln("[dual] The incoming and outgoing of `vertex` are: %s and %s",
dualVertex.incoming(), dualVertex.outgoing());
writefln("[dual] The src and target of `edge` are: %s and %s",
dualEdge.src(), dualEdge.target());
mixin UseModel!("ddualVertex", dualVertex, DualGraph!(V, E, VId, EId), "V");
mixin UseModel!("ddualEdge", dualEdge, DualGraph!(V, E, VId, EId), "E");
writefln("[dual-dual] The incoming and outgoing of `vertex` are: %s and %s",
ddualVertex.incoming(), ddualVertex.outgoing());
writefln("[dual-dual] The src and target of `edge` are: %s and %s",
ddualEdge.src(), ddualEdge.target());
}
alias VertexId = uint;
alias EdgeId = string;
class Vertex {
EdgeId[] incoming() { return ["i1", "i2"]; }
EdgeId[] outgoing() { return ["o1", "o2"]; }
}
class Edge {
VertexId src() { return 0; }
VertexId target() { return 1; }
}
useBothGraphAndItsDual
!(Vertex,Edge,VertexId,EdgeId)
(new Vertex(), new Edge());
/* OUTPUT IS:
The incoming and outgoing of `vertex` are: ["i1", "i2"] and ["o1", "o2"]
The src and target of `edge` are: 0 and 1
[dual] The incoming and outgoing of `vertex` are: ["o1", "o2"] and ["i1", "i2"]
[dual] The src and target of `edge` are: 1 and 0
[dual-dual] The incoming and outgoing of `vertex` are: ["i1", "i2"] and ["o1", "o2"]
[dual-dual] The src and target of `edge` are: 0 and 1
*/
[1] : The whitepaper for Genus can be found at https://www.semanticscholar.org/paper/a1d0c109155a9040f6b24355806f123744f3c841