-
-
Notifications
You must be signed in to change notification settings - Fork 111
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
Record type to express key/value pairs #447
Comments
For the record, Kyle and I discussed this requirement and discussed the following motivations for not adding this feature:
Up until this point, users have satisfied similar requirements with either:
concept Pair {
o String key
o Foo value
}
concept MyConcept {
o Pair[] variables
} However, this changes the performance characteristics (i.e. lookup is now Similarly, we've also considered supporting JSON objects as a Scalar type in the past, however, this was abandoned because it encouraged untyped properties which hinders introspection by clients. In TypeScript, note that However, Kyle's requirement strikes a compromise between the extremes of untypedness and strict typing.
|
I support this. We should also discuss the naming and syntax. Do we prefer Is it possible to adopt a different syntax which is less technical for the Generics requirement? Particularly given the constraint to only allow o Dictionary<Address> address |
Thinking aloud...
|
I don't think we should just limit to having the user only pass in the ValueType, I think that's very limiting for possible future scenarios where a user can pass in other String derivative types (e.g. String literals comes to mind), as keys. I also don't think it allows for passing in an Enum as a key type which I think is a requirement. |
Also I am not sure that |
I agree with Matt's proposed simplification to only annotate the value type and not the key type. I could be wrong but I personally don't see the enum key type as a very compelling case. The necessity of the Record type comes from the need to express unknown keys, which is currently impossible in Concerto. Expressing a set of known keys (as from an enum) can already be done efficiently by declaring them as optional fields of a concept. For example, the following seem equivalent to me:
and
|
But what (if in the future) you had other String derivate types other than Enums? For example, String Literals come to mind. How then would you express the String Literal being the Key of a That's why I proposed we should express the Key, Value pair for future flexibility. I think this also aligns it with practically every typed programming language that exists which expresses the type of a Record using Key/Value pair. I also don't consider the two examples you showed to be strictly equivalent. You shouldn't allow for optionality generally with a Record type. You can see the different in this TypeScript playground here. I think what you're trying to express is a |
In that case, isn't it equivalent to this?
To me it seems rather strange to me to imply that Record<K, V> should "generally" require all possible values of K to be specified. DateTime is a string derivate, but obviously for Record<DateTime, T> they shouldn't have to specify every possible DateTime key. For Record<String, T>, the key space is infinite. Surely the general case is that the user isn't required to specify every possible key value. Requiring all possible values when the key is an enum, then, would be inconsistent with the general case. (I do observe that typescript treats enums differently than strings and other types when used as a record key.)
I agree that it's more useful to be able to express both the key and value. The question is more about whether the Concerto team wants to do the additional work. (I just don't think the enum example is all that compelling, given that it seems like it can already be expressed in other ways.) To the previous point, I think "practically every typed programming language that exists which expresses the type of a Record using Key/Value pair" doesn't require every key to be specified, though, even when the key is an enum type. Either that, or the number of such languages must be very small. (To my mind "string literal" means a string specified literally as opposed to being the value of some other expression, but I guess you have a different meaning in mind. I don't know quite what you mean, but I'm guessing it sounds like an enum.) |
I'm not sure how you would access a key in your example based on the enum key. For example, how would you do
Agreed! Which is why I think passing a Key for a Record is a necessity, because types need to be context aware based on what they are configured for.
I think this is because very few languages express a true Record type, while many express a Map type or Dictionary type. This is because of the historical tie between JSON serialization and how JavaScript/TypeScript express Records/objects. For example, even in TypeScript a Map doesn't require every Key to specified when using an Enum. You can see that in this example. Since Concerto is trying to express models that will be serialized, a Record type is clearly the correct choice, you can't serialize a Map.
You can see the documentation on this here and here. A literal type is Enum like when combined with union types but can be expanded with template literals to be very powerful. You can see an example of this here. Just to be clear I'm not proposing we add string literals into Concerto here, we can discuss that in another issue. But I do think we want the flexibility to take advantage of these types of features with Records in the future so we can express truly type safe models. |
Just to add to this, since the release of Scalars in the Concerto language I think this increases the need for a Record type to be able to specify both a Key and a Value type. I can think of lots of examples where wanting to do something like this could make sense:
|
I agree with that. |
Syntax AlternativesWe should choose one of the following variants. // Variant 1: Type Variable syntax
// Inspired by TypeScript, C# etc.
concept MyConcept {
o Record<GUID, Person> people
}
-----------------------------------
// Variant 2: Modifiers
// "Concerto-like"
concept MyConcept {
o Person[] people index GUID
}
-----------------------------------
// Variant 3: map keyword
// Inspired by Smithy
// https://smithy.io/2.0/spec/aggregate-types.html#map
map AddressBook {
o GUID key
o Person
}
set MyFruit {
o Fruit
}
// Individual or Corporation or Charity
// Individual | Corporation | Charity
union Party {
o Individual
o Corporation
o Charity
}
concept MyConcept {
o AddressBook people
}
-----------------------------------
// Variant 4: map keyword
map AddressBook of Person // Implicit String type variable for keys
map AddressBook from PhoneNumber to Person
concept MyConcept {
o AddressBook people
}
-----------------------------------
// Variant 5: dictionary & record keywords
dictionary AddressBook for Person // Implicit String type variable for keys
record AddressBook of Person keyed by GUID
concept MyConcept {
o AddressBook people
} All options represent the same conceptual structure, e.g. {
"123-34-12-1234": { "$class": "Person", ... },
"234-45-56-1234": { "$class": "Person", ... }
} Definitionsconcept A {}
concept B identified {}
enum E {
o ONE
o TWO
}
scalar StringScalar extends String
scalar BooleanScalar extends Boolean Spec for Record TypesExamplesThese examples use the type variable syntax (e.g. with implicit generic type declaration of concept Concept {
o Record<String, String> record1
o Record<String, Boolean> record2
o Record<DateTime, Boolean> record2b
o Record<String, A> record3
o Record<StringScalar, String> record4
o Record<E, String> record5
o Record<E, String>[] record6
o Record<String, String> record7 optional
// FOR DISCUSSION
// New `Relationship` type
o Record<String, Relationship<B>> recordOfRelationships
--> Record<String, B> recordOfRelationships2
// New `Array` type to make aggregate operations consistent
o A[] listOfAs
o Array<A> listOfAs2
o Record<B, String> recordWithIdentifiedConceptKeys
o Record<String, Record<String, String>> nestedRecord
o Record<Integer, String> integerKeyedRecord
// NOT SUPPORTED
// o Record<A, String> record
// o Record<BooleanScalar, String> record
// --> Record<String, String> record // Relationship target is not identified
// --> Record<String, A> record // Relationship target is not identified
// o Record<String, String> record1 default={"a":"A","b":"B"}
}
// FOR DISCUSSION
// Suppose that there's an existing concept in an old model called "Record". Then should the following be valid
concept Record identified{} // A legacy concept called "Record"
concept MyHeadHurts {
o Record foo
o Record<String, Record> bar
}
// Using scalars to declare type aliases
scalar Dictionary extends Record<String, String>
concept Library {
o Dictionary[] dictionaries
} Future ExtensionsTo check that the design is extensible to other potential aggregate types in future Examplesconcept SetConcept {
o Set<String> stringSet
o Set<Boolean> booleanSet // Although why?
o Set<Integer> intSet
o Set<Double> doubleSet
o Set<Long> longSet
o Set<E> enumSet
o Set<B> conceptSet
o Array<Set<String>> stringSetArray
o Set<StringScalar> stringScalarSet
o Set<BooleanScalar> booleanScalarSet
// NOT SUPPORTED
// o Set<A> record // Concept is not identified
}
concept UnionConcept {
o Union<A,B> ab
o Union<A,B, C> abc
} |
At least to me I think the "Modifers" syntax looks a little confusing just because I would expect that to be a Person array and not a Record. Between the other two options I don't have a really strong opinion, but have a preference towards the first option, only because that aligns well with other languages. The map keyword is also just "more" code to represent the same thing, but that's not necessarily a bad thing per say.
I think you mean extends Boolean here yes?
I'd personally say no, adding keywords and needing to modifying legacy code to not utilize those keywords I don't think is a foreign concept and I think would make for the cleanest break here. |
Some quick notes, as I won't be able to attend the meeting tomorrow. I'd be in favour of:
|
Based on the feedback above and the discussion today on the Working Group call. I've started drafting a specification for this feature. https://github.com/accordproject/concerto/wiki/Aggregate-Types-Design |
@mttrbrts thanks for putting that together. I think what you have in that spec generally looks good to me. A few pieces of feedback:
I feel like we might want to be explicit about what properties specifically is the value of the
I think this better aligns with how other concepts are written in Concerto in the sense that they always have a type and a name, in this case the names are just key words. It just would read odd to me to not have a name for a property.
When you say implemented here, do you mean that internally to Concerto it might represent this as a Map object, or that if this is expressed in a concrete language like TypeScript it would actually be a If it is also a requirement to be able to express a true Map object in the concrete language, I think then we ought to have two different types here, |
Thanks @KyleBastien
I don't have a strong view on this. However, it's worth comparing this to our syntax for Enums, and what is proposed for Union Types
I'm only referring to the internal representation, I'll add the requirements for TypeScript to the Code Gen section
We don't have this requirement, AFAIK. |
Agreed I don't have a really strong feeling about this, it just read a bit odd to me to mix the two in the same concept.
Sounds good, adding the code gen requirements would be appreciated. From what I see that you updated it looks good so far.
Sounds good, we might want to consider if we will have this requirement in the future but given that it's quite hard to serialize true |
Good point, I've updated the spec to remove the modifiers entirely and to rely on the order instead. |
This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 10 days. |
Feature Request 🛍️
This proposes the addition of a Record type into the Concerto syntax to be able to express key/value pairs in an object. For example a
Record<String, Foo>
would express a key/value pair where the keys of the objects areString
's and the values of this object areFoo
objects.You can look more at TypeScript's Record type documentation here: https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type, which I think is good inspiration for what should be possible here.
Use Case
Here are a variety of different use cases that I think we should support:
Example 1:
Example 2:
Example 3:
Possible Solution
Not sure about implementation here, I would assume that we would at least need to be able to support the concept of generics within the language as a pre-requisite to this.
I also think it would be good to constrain the Record type to only support
String
orString
derivates (e.g., Enum) as keys for a Record type. This is similar to what TypeScript does, and translates well to JSON. This does limit Record's functionality in terms of what is possible in other languages like Java or C# which could use classes as key types, but I think this constraint is desirable.Context
Currently describing key/values pairs where keys are user generated content is currently not possible in Concerto. A Record type would help solve that problem. We see this when trying to describe variables for example, where variable names are defined by the user.
In these scenarios it's still very useful to developers at development time to have a representation of these Key/Value pairs, even if they aren't fully type safe, because a developer needs to understand what is possibly on this property of a concept.
The text was updated successfully, but these errors were encountered: