Is it possible to have methods inside of records?

I already know the answer to this question; i.e since functions are objects they can't be inside of records or tuples.
But what I really wanted to know is whether or not it's possible to have methods in records in some way?
I would say methods declared within records are frozen by default.
so something like:

const myRecord = #{
    myMethod() { }
}

// this get's converted to
const myRecord = #{
    myMethod: Object.freeze(function() { })
}

The context / 'this' keyword inside of the function would work as it would in any frozen object.

Is it a viable option? I just really want methods inside of records because without them I don't really see myself reaching out to them over an array or pojo.

No, it’s not viable. They could close over an object and return it.

So you're saying someone could do something like:

const myFunction = () => null
myFunction.foo = "bar"

const myRecord = #{
    myMethod: myFunction
}

Or

let myFunction
{
    myFunction = () => ({ foo: "bar" })
}

const myRecord = #{
    myMethod: myFunction
}

I don't see either of them as an issue as the first one is not really a proper js function rather an exotic one; the second is just a function returning an object; I don't know how that affects the mutability of the record.

No, like () => Object.prototype, for example. Primitives need to only be able to produce objects inside their own realm, and that function captures the realm it’s created in.

But how does that affect the mutability of a record?

// code in iframe
const myRecordX = #{
    myMethod() { }
}

// code in main window
const myRecordY = #{
    myMethod() { }
}

console.log(myRecordX === myRecordY)
// so you're saying it might return false?

So one more thing; if it were to work, how would equality work;
(#{ f() {} }).f === (#{ f: () => {} }).f // how does this work?
This is just a concern I had ;)

It doesn’t, that’s not the only constraint. Records also have to be able to pass from one real, to another without exposing any mutable objects.

Having direct references in R&T to objects is currently not possible.

Besides the problem of Realm specific references that @ljharb, primitives simply cannot provide any direct way to get any object reference as that would break too many assumptions by existing code.

Could you elaborate on your use case motivating methods attached to R&T? Since you use the word "method" and mention getting the current R/T as context, are you looking to attach behavior to R&T of a specific shape?

For example, let's assume we could attach a prototype to some records:

const BigDecimalProto = {
  multiplyBy(other) {
    return #{
      __proto__: BigDecimalProto,
      n: this.n * other.n,
      m: this.d * other.d
    };
  },
};

const bigDecimal = #{
  __proto__: BigDecimalProto,
  n: 7n,
  d: 13n,
};

bigDecimal.multiplyBy(#{ n: 5n, d: 3n });

This is getting close to having value classes, but as mentioned, directly referencing objects from primitives is not possible and not portable across realms. One approach may be to define and export this prototype in a module expression, which can define behavior without closing over a Realm. This is in fact one of the approaches explored by the champions of the share structs proposal, and some delegates wondered if there may be more possible synergies between these proposals.

If that were to work there would need to be some way to say if two functions are equal in a way that was both intuitive and useful

2 Likes

So functions need to have an internal hash for checking equality?
I get the deal that it complicates the proposal a lot.
I mean I can still do Object.assign(Record.prototype, { }) if I want some methods to be attached to records, i.e if I want it so badly right?

I mean could the same be done using:

#{
    __proto__: { }
}

It is discouraged to change the prototype of builtins but if I absolutely want to then I should be able to do it in some way right?
So according to @mhofman is it something that the champions behind the proposal have considered?

I figured a workaround instead of this:

const myRecord = #{ }
const myPureObject = {
    ...myRecord,
    myMethod() { }
}

We should atleast be able to spread the properties of a record into a plain object.
That also raises the question:
Will there be a way to convert a Record to a plain object?

Record.prototype is null specifically to avoid prototype pollution.

Can do either { ...record }, or Object.fromEntries(Object.entries(record))

Thanks for that; I guess spreading a record inside of an object seems like a fine workaround.

To be clear, my example above was hypothetical, trying to understand what you were looking for. Attaching a behavior to R&T is not something the champions of the R&T proposal have considered, and it probably would not take the shape of using __proto__.

The committee did however ask the champions how R&T might relate to the structs proposal, and if R&T as primitive may not put us on a path to full value types.

I am still curious to understand what you're trying to accomplish by wanting methods on R&T, especially comparable ones.

If you're just trying to add behavior to a record/tuple, you may have to think about it through a different lense.

Normally, one adds behavior to an object by simply attaching that behavior to its prototype. Thus, you would be able to do something like this:

// point.js
export class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  updateX(newX) {
    return new Point(newX, this.y);
  }
}

// main.js
import { Point } from './point.js'
const myPoint = new Point(2, 3)
  .updateX(5);

With records/tuples, you have to think about the solution a little differently. Instead of tacking on the behavior to the prototype, you just export your desired behaviors, like this:

// point.js
export function create(x, y) {
  return #{ x, y };
}

export function updateX(newX) {
  return create(newX, this.y);
}

// main.js
import * as point from './point.js'
const myPoint = point.updateX(
  point.create(2, 3),
  5,
);

Remember that you can always use the pipeline operator as well, when that comes out, to help avoid nesting.

// main.js
import * as point from './point.js'
const myPoint = point.create(2, 3)
  |> point.updateX(%, 5);

This sort of solution is very common in functional languages (where records/tuples are inspired from). Really, the only thing you'd be missing from a solution like this is polymorphism, which, if that's needed, that could certainly be an interesting discussion.