5

In C++, I can instantiate a generic type at compile time and then construct it at runtime:

struct Vertex {};
struct Edge   {};

template<typename NodeType, typename IdType>
struct Wrapper {
  IdType id;

  Wrapper(IdType id) : id{id} {};
};

int main() {
  using vertexWrapper = Wrapper<Vertex, int>;
  vertexWrapper v(3);
}

The variables are clearly separated, and types never look/feel like values. I am trying to do something similar in Chapel:

record Vertex {}
record Edge   {}

record Wrapper {
  type nodeType;
  type idType;   
  var id: idType;

  proc init(id) {
    this.id = id;
  }
}

type vertexWrapper = Wrapper(Vertex, int);

var v1 = 3: vertexWrapper;

When I compile this code, I get:

chpl -o graph-add-inclusion --cc-warnings test.chpl
test.chpl:9: In initializer:
test.chpl:10: error: can't omit initialization of field "nodeType", no type or default value provided
test.chpl:10: error: can't omit initialization of field "idType", no type or default value provided

Is there a way to separate type construction from value construction in Chapel to achieve an effect of a tagged type as I am trying to get in my example? I use tagged types to have single implementation that's common for two kinds of entities (vertices and edges here), but I want these entities to be different types.

There is a further, related question. Should I be able to just write:

type vertexWrapper = Wrapper(Vertex);

and then have integer deduced separately from my constructor?

It seems that constructors are checked at definition time without the possibility that types can be provided separately from values. Did I get this right, and, if I did, is this something that will change in the future?

Marcin Zalewski
  • 512
  • 3
  • 8
  • 2
    Where you write: `var v1 = 3: vertexWrapper;` note that Chapel doesn't currently have an official way for users to write their own casts like this at present and doesn't conflate initializers as casts as in C++ (issue #8242 on our GitHub explores this—https://github.com/chapel-lang/chapel/issues/8242). User-defined casts are a feature that's planned for the future, and can be accessed in an unofficial and somewhat ugly form at present. I'll PM you with more info on that. – Brad Apr 19 '18 at 22:30
  • Thank you. I tried to remove the field, and just have the default intializer with the declaration changed to `var v1: vertexWrapper; `, but I still get the same error. Does your comment apply to this situation as well? – Marcin Zalewski Apr 19 '18 at 22:38
  • 2
    Yeah, I didn't mean to imply that I had a good answer to the original question (or I would've answered it :) ). Simply noting that the since user-defined casts aren't supported, that line isn't likely to work. Without the user-defined cast feature, I think you'd want to write that line as `var v1 = new vertexWrapper(3);` But that doesn't address the root problem you're wrestling with... – Brad Apr 19 '18 at 22:52
  • 1
    FWIW, I agree with the spirit of this post that, once a type alias refers to an instantiation of a generic type, there ought to be a way to invoke an initializer without supplying redundant generic fields. As you are likely aware, Chapel's initializer story ("constructors v2") is just coming on-line and this is something about it that needs improving, I think. – Brad Apr 20 '18 at 23:54

2 Answers2

4

The issue you are encountering is with the initializer you have defined, rather than the type alias you are using. Because of how you have defined it, a user could conceivably try to write this:

var x = new Wrapper(1); // type is inferred from the new's return

and the initializer wouldn't have a clue what to do about the nodeType and idType fields. So your initializer needs to explicitly set them within its body.

I agree that it would be nice to have the initializer figure out their values when you are using an instantiation, and anticipate that this is something we will support in the future. In the near term, you can mostly get what you want by a little bit of duplicated work.

To do that, first you could update the initializer so that it can figure out the idType based on the corresponding argument.

proc init(id) {
  idType = id.type;
  this.id = id;
}

However, that doesn't help you with the nodeType field, since there is no corresponding value field the initializer can rely upon in what you've described. Which means that you will have to provide it to the initializer by hand. You can do this in a general way from your type alias by accessing its nodeType field:

var v1 = new vertexWrapper(vertexWrapper.nodeType, 3);

But you'll also need to update the initializer to take that as an argument. So creating the instance v1 will have to look something like this:

record Vertex {}
record Edge   {}

record Wrapper {
  type nodeType;
  type idType;   
  var id: idType;

  proc init(type nodeType, id) {
    this.nodeType = nodeType;
    this.idType = id.type;
    this.id = id;
  }
}

type vertexWrapper = Wrapper(Vertex, int);

var v1 = new vertexWrapper(vertexWrapper.nodeType, 3);

Hopefully that's a solution that works for your use case in the near term.

Should I be able to just write:

type vertexWrapper = Wrapper(Vertex);

and then have integer deduced separately from my constructor?

We don't support partial instantiations at this time, so Wrapper(Vertex) isn't something you can write. If you want idType to be set to a common value like int, you can provide it as a default value to the type field:

type idType = int;

which would allow you to just specify nodeType via Wrapper(Vertex), but would mean that idType is locked into int unless you create the instantiation using new (var x = new Wrapper(Vertex, 3.0);), or unless you specify something else at the same time as nodeType.

Community
  • 1
  • 1
Lydia Duncan
  • 595
  • 2
  • 8
  • 2
    Following up on Lydia's response, note that if you don't actually require the `idType` field, you could also simply write this record as: `record Wrapper { type nodeType; var id; }` (i.e., make `id` completely generic rather than of a specified type constraint. Your initializer could then constrain `id` to only be certain types if you wanted). – Brad Apr 19 '18 at 22:54
  • Thank you for your answer. Based on what you wrote, I ended up implementing `makeVertexDesc` and `makeEdgeDesc` methods in my class that take the `id` part, and provide the `nodeType` part to the initializer. That's a good workaround until initializers can draw information from instantiations. – Marcin Zalewski Apr 23 '18 at 20:49
1

As a preface to this answer (and as noted in my comments on the question), I think that Chapel's initializers story ought to improve in order to support initializer calls on type aliases representing instantiated generics as you're trying to do.

Until then, here's an idea I tried that seems to work [Try It Online]. Essentially, I'm creating a factory method, newNode() on the Wrapper type itself by using a type method—i.e., one that's called on the type itself rather than on a value of that type:

record Vertex {}
record Edge   {}

record Wrapper {
  type nodeType;
  type idType;
  var id: idType;

  proc type newNode(id) {
    return new Wrapper(nodeType, idType, id);
  }
}

type vertexWrapper = Wrapper(Vertex, int);

var v1 = vertexWrapper.newNode(3);
writeln(v1);

Because the vertexWrapper type alias has nodeType defined, I can refer to it within the factory method in order to create new instances of Wrapper.

I think that this could essentially be viewed as a different way to encapsulate the solution that Lydia proposed previously.

Brad
  • 3,839
  • 7
  • 25
  • 1
    Thank you Brad. I rewrote my code to use this idea. I find it the most elegant in the sense that I don't have to specify the same information more than in one place, which is something that usually leads to errors in the future. – Marcin Zalewski Apr 23 '18 at 21:47
  • 1
    Cool. We'll keep working on improving initializers to the point that you can do something more direct with that type alias without this workaround... – Brad Apr 23 '18 at 23:46