This proposal is intended to be a follow-on proposal for record/tuples. It's loosely inspired by a similar "struct" idea that I had brought up previously as part of a separate thread, but I decided to break it out and simplify it a bunch.
Here's the basics of how it works:
// Create a new tag
const userTag = new Tag()
// Tag a record
// tagging a record that's already been tagged will
// cause the old tag to be stripped off.
const user = userTag(#{ name: 'Sally', age: 23 })
// Checking if a record is tagged by the userTag
userTag.isTagging(user) // true
userTag.isTagging(#{ x: 2 }) // false
user === userTag(#{ name: 'Sally', age: 23 }) // true
user === #{ name: 'Sally', age: 23 } // false
The ability to tag a record like this provides the end-user with a tool that can be used to solve a wide variety of problems. Here are a couple of specific benefits that come from having a tagging system.
Brand Checking
The ability to tag records provides the end-user with the ability to know, with certainty, who created a particular record. Take this example:
// user.js
const userTag = new Tag()
export const create = ({ name, age }) => userTag(#{
name,
age,
birthday: getBirthdayFromAge(age),
})
export const isUser = record => userTag.isTagging(record)
Notice in the above example, the userTag is never exported. This means, the only way an outside user can create a record that's been tagged with the userTag is via the exported create function. At any point in time, the end-user can utilize the isUser() function to verify that a record they hold was, in fact, created by this user module. If it was, they can know with certainty that, for example, the birthday and age values are correctly in sync.
Variants
Variants are a much-loved feature among functional programmers, and they can easily be emulated via tagged records. For the purposes of this example, we're going to assume that Tag() can accept a validation function - this isn't necessary, but it could be a nice-to-have feature. If validation fails, an error will be thrown.
// Emulating a variant
const maybe = {
just: new Tag({
validate: record => (
Object.keys(record).length === 1 &&
'value' in record
),
}),
nothing: new Tag({
validate: record => Object.keys(record).length === 0,
}),
}
maybe.just(#{ x: 2 }) // Error: Failed the validation check.
function findInArray(array, predicate) {
for (const element of array) {
if (predicate(element)) {
return maybe.just(#{ value: element })
}
}
return maybe.nothing(#{})
}
findInArray([3, 4, 5], x => x % 2 === 0).value // 4
// We could allow tags to work with the pattern matching proposal as well
match (findInArray([3, 4, 5], x => x % 2 === 0)) {
when (maybe.just with { value }) { value }
when (maybe.nothing) { throw new Error('Value not found!') }
}
Typed superset of JavaScript, like TypeScript, can add a lot of additional value to this proposal, e.g. by letting you provide a type definition for each variant case, and by requiring match statements to exhaustively account for all cases of a variant, so if you ever add a new variant case, your compiler can warn you of all places in your code that you need to update.
For reference, I'll also point out this other thread which also tried to introduce variants, but in a significantly different way.
Other notes
It's technically possible to emulate this concept of tags to a degree in userland. Check out the previous discussion where this idea came from to see some ways in which it can be done via WeakMap, if you're willing to put a box into your record. It's just not easy or convenient to do this kind of thing. Having a tagging system as a first-class language feature puts a lot more power at your fingertips.