Skip to content
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

Allow records to implement interfaces #3071

Closed
g5becks opened this issue May 12, 2023 · 7 comments
Closed

Allow records to implement interfaces #3071

g5becks opened this issue May 12, 2023 · 7 comments
Labels
feature Proposed language feature that solves one or more problems state-rejected This will not be worked on

Comments

@g5becks
Copy link

g5becks commented May 12, 2023

I have been trying out the new records feature, and while they are a definite upgrade to the language, they seem somewhat non intuitive to use for someone coming from programming mostly in F# and Typescript.

Consider the following code,

typedef Quote = ({
  DateTime date,
  Decimal open,
  Decimal high,
  Decimal low,
  Decimal close,
  Decimal volume
});

typedef SMAResult = ({DateTime date, double result});

abstract interface class TSeries {
  DateTime get date;
}

typedef Series<T extends TSeries> = Stream<T>;

// This wont work, you'd have to create an entire
//  class when a record would be simpler
// as there might be many different simple variations of the T constraint.

Series<Quote> createSeries(Iterable<Quote> quotes) async* {
  for (var quote in quotes) {
    yield quote;
  }
}

Stream<SMAResult> simpleMovingAverage(Series<Quote> inputStream, int lookBack) async* {

  List<double> window = [];
  await for (Quote value in inputStream) {
    window.add(value.close.toDouble());
    if (window.length > lookBack) {
      window.removeAt(0);
    }
    yield (date: value.date, result: window.reduce((a, b) => a + b) / window.length);
  }
}

consider there is a library full functions that return different result types similar to the SMAResult, lets say I want to accept anything with the shape

({DateTime date, double value})

as opposed the the Quote type in the simpleMovingAverage function, which would allow calculation of results from other functions that do similar calculations.

// will not work
Stream<SMAResult> simpleMovingAverage(
    Series<({DateTime date, double result})> inputStream, int lookBack)

In situations like this, where it's much simpler to use records as opposed to classes, mainly due to the structural equality aspects of the type being implemented for you, it would be nice for records to be able to implement interfaces.

Coming from F# this is kind of second nature

type ISeries =
    abstract member Date: DateTime with get

type SMAResult =
 {
    _date: DateTime
    Result: double
}
interface ISeries with
        member x.Date = x._date

converted to dart I am guessing that would be something similar to

typedef SMAResult = ({@override DateTime date, double result}) implements TSeries;

in typescript this is all much easier because the entire type system is structural so every
type is an interface itself, which makes thing a lot simpler, no interfaces to define etc.

Currently, not being able to use records in these types of contexts seems somewhat limiting IMO, if I only need a bag of values and no methods, but I need to ensure every type has some subset of fields, it's not currently possible (that I am aware of ) in the current implementation of records.

Another option (that does't seem feasible) is so called "Statically Resolved Type Parameters" which are simply member constraints, and allow a form of duck typing.

// Require `TResultA to have a member Date of type DateTime

let inline syncIndex<'TResultA, 'TResultB when 'TResultA: (member Date: DateTime) and 'TResultA: equality>
    (syncMe: 'TResultA alist)
    (toMatch: 'TResultA alist)

Maybe there is a way to accomplish the same effect that I am unaware of? If not, I would consider this to be a nice feature to have and allow better overall use of records.

@g5becks g5becks added the feature Proposed language feature that solves one or more problems label May 12, 2023
@lrhn
Copy link
Member

lrhn commented May 15, 2023

Records are like very simple classes that already exist. You cannot make them implement new interfaces.
They are structural types, which means that there is no declaration where you can add an interface to them.

A Dart typedef doesn't create a new type, and it cannot add interfaces to existing types (that requires a new type).

Essentially, it does not make sense to add interfaces to records, Dart records are simply not something you can add interfaces to, not any more than you can add interfaces to function types.

@lrhn lrhn closed this as completed May 15, 2023
@lrhn lrhn added the state-rejected This will not be worked on label May 15, 2023
@munificent
Copy link
Member

@lrhn is right. While it might be tempting to use a typedef on a record type as a lightweight way to define a new type, that's going against the grain of the language.

A typedef is just an alias for an existing type. It doesn't create a new type with its own properties and capabilities. If you want that, a class declaration is what you want.

@g5becks
Copy link
Author

g5becks commented May 20, 2023

@lrhn is right. While it might be tempting to use a typedef on a record type as a lightweight way to define a new type, that's going against the grain of the language.

A typedef is just an alias for an existing type. It doesn't create a new type with its own properties and capabilities. If you want that, a class declaration is what you want.

Understandable, but to be fair, coming from any other language that implements a "record" type (F#, C#, Java), this is pretty non-intuitive IMO. I am okay with the fact that this is not possible, but this is definitely not in line with the ethos of Dart being familiar to those coming from other languages. E.G.

public interface IData<T>
{
   T Data { get; }
}

public record PersonWithData<T> : Person, IData<T>
{
   public T Data { get; init; }
}
public record EnhancedList<T>(List<T> inner) implements
       ForwardingList<T>,
       Mappable<T>,
       Filterable<T, EnhancedList<T>>,
       Groupable<T> {}
 
interface ForwardingList<T> extends List<T>, Forwarding<List<T>> {
   List<T> inner();
   //…
}

I'd also add, that is doesn't make much sense (to me at least) that records can't be used in sealed class hierarchies.

I guess records are meant to be more of a tuple type than a data class? I guess that would make sense if dart had actual primitive types, but the limitations placed on records (as of now) seem to be counter productive and pretty limiting. Was hoping to not have to keep using Freezed, or Equatable, etc, but the goal of records seems to be aimed at solving a different set of problems (or at least only a subset). Anyhow, thanks for adding them.

@lrhn
Copy link
Member

lrhn commented May 20, 2023

I guess records are meant to be more of a tuple type than a data class?

Not just "more of". Records are precisely tuples, nothing more, nothing less. The only fancy part is that some of the positions can be named with an identifier instead of an integer. It's still just a product type. And there are structural types, meaning that they do not have a name, they are defined entirely by their structure, which is why they cannot implement interfaces or be part of class hierarchies.

Dart records are not intended to be data classes. That's why they're not very good at it. They're low-level building blocks, which makes it easier to, say, return more than one value. They're closer to an abstraction over an argument list, than to a class instance.

The C# records are immutable classes or structs with automatic field based equality. A record struct it's very close to a Dart record in equality and identity behavior, but it is still a nominative type. If you declared two different record structs with the same field names and types, they're not the same type, and not automatically signable to each other.

@g5becks
Copy link
Author

g5becks commented May 20, 2023

The C# records are immutable classes or structs with automatic field based equality. A record struct it's very close to a Dart record in equality and identity behavior, but it is still a nominative type. If you declared two different record structs with the same field names and types, they're not the same type, and not automatically signable to each other.

But C# record structs have with expressions, can use annotations, implement interfaces, etc.

So I guess the only confusion (on my part) is the naming of Dart records creates the expectation that a record would be something more akin to what one is used to when coming from any other language that provides a similar construct.

Not just "more of". Records are precisely tuples, nothing more, nothing less.

What's the reasoning behind not just calling them tuples then? I mean, outside of the Database world - the two aren't really interchangeable (in terms of naming at least) that I am aware of, and when taking into account the features that a record abstraction has in pretty much every language prior to Dart , it seems like an odd naming decision.

Anyhow, I am guessing that the plan is to leave actual data classes as an issue to be solved by libraries? Or something to be tackled when the static meta programming feature is complete?

@munificent
Copy link
Member

But C# record structs have with expressions, can use annotations, implement interfaces, etc.

"Record" in C# and Java is really more like "data class" in Kotlin. It's a separate feature (with a confusingly chosen name).

We're interested in exploring those capabilities for Dart too, but it's different from records. See this PR for one piece of this.

@lrhn
Copy link
Member

lrhn commented May 23, 2023

Not just "more of". Records are precisely tuples, nothing more, nothing less.

What's the reasoning behind not just calling them tuples then?

Tuples suggests only integer positions, the "record" name is supposed to suggest that you can also use named positions. It's an existing word already used to describe name-indexed product types.

The distinction between records-where-all-keys-are-consecutive-small-integers and tuples, or records and tuples-where-fields-can-be-accessed-by-name is mostly perception. The underlying structure is a product type, the names of projection functions is just syntactic sugar.

(But not entirely, since it allows ({int x, int y}) and ({int length, int width}) to be different and unrelated types, while still both being isomorphic to int × int.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems state-rejected This will not be worked on
Projects
None yet
Development

No branches or pull requests

3 participants