google / go-jsonnet

Apache License 2.0
1.62k stars 234 forks source link

feature request - key of object within parent object #355

Open ghostsquad opened 4 years ago

ghostsquad commented 4 years ago

I've found myself often doing things that require the key of the object I'm currently in. Here's an example:

top: {
  myThing: {
    name: "myThing" // this is the key of the current object with "top"
  }
}

desired:

top: {
  myThing: {
    name: keyOf(self, $)
  }
}

I have no idea if this is reasonably possible, or maybe there's a better way to go about doing this?

ghostsquad commented 4 years ago

I suppose an alternative would be a pattern like:

withElement(name):: {
    [name]: {
        name: name
    }
}
top: $.withElement(name) { }
ghostsquad commented 4 years ago

the only problem with the alternative is that I don't foresee a way to get autocomplete.

sbarzowski commented 4 years ago

I agree that it can be sometimes annoying to repeat the name, especially since one time it's a string, so it may be quite easy to silently make a typo (more realistically, miss the last character when copying in case of long names).

My pragmatic recommendations for now: 1) If the name is short just repeat it. It's going to be less trouble and more readable this way (it may still be worth creating a field "name". 2) If the name is long you can put the name in a variable. This variable needs to live outside of object (you cannot use object locals for that, for good reasons, but unfortunately in this case). 3) If you're creating the object outside of it's parent (you have it in a variable), then you can just do something like:

{
    [obj.name]: obj
}

(Again, object locals cannot be used for that).

I don't get understand withElement. I imagine withElement should also take an object as an argument, right? Or would you create a separate function like that for each sort of object that you would want named?

I'm going to write later about some challenges and approaches we can take to improve the current situation.

ghostsquad commented 4 years ago

Thank you for your reply. I'm trying my best not to repeat the name value, in order to ensure if it were to be updated, it gets updated in all the right places.

I'm trying to figure out idiomatic ways to create libraries, and deciding on whether or not a client cares about the value, or simply needs a way to reference it. Currently, the client doesn't care about the value itself, but does need to reference it to create a dependency graph.

Another (better) example:

local l = import "./lib.libsonnet"

local jobs = {
   J1: l.jobFoo()
   J2: l.jobBar(requires = self.J1.name)
}

And the missing part of this is the library itself, which needs to cross reference jobs as well.

ghostsquad commented 4 years ago

I think you are right, that it's a little more verbose. Shortcuts are probably just going to make it harder to read or refactor.

sbarzowski commented 4 years ago

Reopening :-). I'll still like to have a nicer syntax for that eventually and I see some options.

sparkprime commented 4 years ago

What you're asking for fundamentally violates referential transparency, e.g. what happens in the case of:

local template = {
  name: magickeyword,
};
top: {
  myThing: template,
  myOtherThing: template,
}

My favourite solution to this problem so far is to pass in the name:

local template(name) = {
  name: name,
};
top: {
  myThing: template('myThing'),
  myOtherThing: template('myOtherThing'),
}

The good thing about this is it allows a syntax sugar for automatically getting at the lexically enclosing field:

top: {
  myThing: template(here),
  myOtherThing: template(here),
}

I'm not sure if this ever got written up as a feature request but I have a doc about it from some years ago... :)

You can see a practical example of this kind of pattern here: https://github.com/google/jsonnet/blob/master/case_studies/micromanage/lib/mmlib/v0.1.2/service/base.libsonnet https://github.com/google/jsonnet/blob/master/case_studies/micromanage/lib/mmlib/v0.1.2/service/google.libsonnet#L402 https://github.com/google/jsonnet/blob/master/case_studies/micro_fractal/fractal.jsonnet#L44

sbarzowski commented 4 years ago

My favourite solution to this problem so far is to pass in the name

This is a very good solution when multiple fields are created basically from the same template/function. It doesn't help much if we have a bunch of fields defined ad-hoc (well, you could create a function for each of them in the object and apply them right after to create a field, but that's pretty awkward). But it should help in most cases where field name repetition is actually a problem.

The good thing about this is it allows a syntax sugar for automatically getting at the lexically enclosing field:

top: {
  myThing: template(here),
  myOtherThing: template(here),
}

I think introducing a syntax sugar like that would help a lot and it is my preferred solution at the moment. I think it would be better to use sth like $fieldname or $field instead of here to eliminate the possibility of conflicts. It makes sense, because $ is also a lexical shortcut (lexically outermost object).

Another solution to this problem that has crossed my mind is to introduce a new type of local, which is scoped such that it is available in both the field name and its body, but not in other fields. You could then do something like:

local name = "myThing"; [name]:: { service: name + "foo" }

or even:

local name = "myThing"; [name + "foo"]:: { service: name + "bar" }

This handles a few more cases elegantly, i.e. when the name is based on a value that's also useful for calculating the body, but using the field name directly would be awkward (e.g. would require stripping some characters). However that is also pretty verbose, and complicates scoping rules for objects, which are already causing confusion sometimes. So I think it's better to do the syntax sugar thing.

sparkprime commented 4 years ago

I also usually have a name field that is automatically bound in the template so that you can just do self.name. You can write a base template that only does that for the ad-hoc case, and in the case of more complex templates you just thread it down to that base template.

top: {
  myThing: NamedObject(here) {
    f: "My name is " + self.name,
  }
  myOtherThing: NamedObject(here) {
    f: "But my name is " + self.name,
  }
}
sparkprime commented 4 years ago

The examples I linked to do that, except they use a hierarchical scheme so you give the parent entity and your local name (which is the same as the field name that you're linked from), and it automatically concatenates all the names in the path from the root using "-" as a separator and makes that available via self.fullName

netomi commented 1 year ago

I did have the same problem as the OP and the discussion triggered some experimentation of myself.

The solution that I came up with so far is to define a function to index an array of objects by a key.

Using a hidden "top_user_defined" key to model the named objects which will get transformed into a key/value mapping.

local indexByKey(arr, key="name") = {
   [obj[key]]+: obj
   for obj in arr
};

top_user_defined:: [
  NamedObject('name1'),
  NamedObject('name2')
],

# could be defined in a base template
top: indexByKey(self.top_user_defined)
sparkprime commented 1 year ago

That works, but is not so good if you want to refer to the NamedObject using their name somewhere else in the code, because you'd have an unstable list index instead of a field on the object. If you need that then you're better off using the kind of solutions Stan & I were talking about further up the thread. But if your objects aren't being referred to, just generated, then you can indeed simply convert from a list of named objects to a dict.

netomi commented 1 year ago

I agree, when you need to reference the NamedObject from somewhere else within the jsonnet files it is a suboptimal solution. However, in my case I need to define named objects that you can override with inheritance and I want to avoid duplicating the name.

Your proposal

top: {
  myThing: template(here),
  myOtherThing: template(here),
}

would be the preferred way for me when I understood it correctly. here is then a reference to the key that you can access e.g. like here.name ?

sparkprime commented 1 year ago

You'd have to do this:

top: {
  myThing: template('myThing'),
  myOtherThing: template('myOtherThing'),
}

The proposed syntax sugar here was to avoid that duplication, but it's not implemented.