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

Make Function generic, and Function<Record, Type> denote a function type. #2478

Open
lrhn opened this issue Sep 13, 2022 · 15 comments
Open

Make Function generic, and Function<Record, Type> denote a function type. #2478

lrhn opened this issue Sep 13, 2022 · 15 comments
Labels
records Issues related to records.

Comments

@lrhn
Copy link
Member

lrhn commented Sep 13, 2022

If we change the declaration of the Function class to class Function<P extends Record, R> and special-case Function<(int, int), int> to mean the function type int Function(int, int), and allowing Function<Record, T> to represent any function with return type T, then we get a way to abstract over parameter list shape, while knowing the return type: Function<Record, int> f is a function which returns an int.

The grammar is free since Function<...> must currently be part of the special form Function<...>(...) for a generic function type, it's a compile-time error to have Function<...> not followed by a parenthesis.

Not all parameter lists can be represented by records. A record cannot represent optional parameters. We can see methods with optional parameters as a kind of union type, and this syntax only produces the leaf types used by those unions.
I still think it will be very useful in some kinds of code (Even just making Function.apply have type R apply<R>(Function<Record, R> f, List positionalArguments, [Map<Symbol, Object?>? namedArguments])) would be an improvement, we could tell the return type from the invocation, without being able to represent the entire function type.

Maybe this is no better than simply giving Function as single type argument, which is the return type, and not try to abstract over argument shapes.

@lrhn lrhn added the records Issues related to records. label Sep 13, 2022
@Wdestroier
Copy link

Should the parameter list come before the return type?

@Levi-Lesches
Copy link

Levi-Lesches commented Sep 14, 2022

Hm, it's usually "Input/Output" but signatures are returnType name(parameters). Given that these don't have a name, maybe it should be "Input/Output", like anonymous functions already are: (parameters) => returnValue. So I'd vote for Function<Record, Type>.

Also, is there an issue open for adding optional values to records? Is that the only difference left between records and parameter lists?

@Wdestroier
Copy link

@Levi-Lesches Great, thanks!

@rakudrama
Copy link
Member

Today, all closures implement the simple non-generic Function interface, and have a function type.
Adding any type parameters to Function would require all closures to implement a more complex interface.
If the parameterized interface type Function<...> can be automatically generated from the function type this might be reasonable to implement.
If the parameterized interface type is subtly different from the function type and cannot be automatically generated from the function type then this will place a code-size burden on the implementation to implement two rich types for each closure. This won't be pay-as-you-go since the compilers have to assume the worst with regards to the usage of most closures.

@natebosch
Copy link
Member

A record type syntax also can't express which named arguments are required

@Levi-Lesches
Copy link

Levi-Lesches commented Sep 21, 2022

Is there a reason records don't copy the grammar/semantics of parameter lists? I see a lot in discussions that records are eventually meant to mirror parameter lists so I'm curious why they're not made to be equivalent from the beginning. Dart already has function subtyping logic (ie, void test({int? a}) is a valid override of void test({required int a})), which can surely be applied to record subtyping as well (Record({int? a}) would be a subtype of Record({required int a})).

@mateusfccp
Copy link
Contributor

A record type syntax also can't express which named arguments are required

Unless we have #2232.

@munificent
Copy link
Member

Is there a reason records don't copy the grammar/semantics of parameter lists?

Records do significantly borrow from parameter lists. The record type grammar is essentially a subtype of the function type parameter list grammar. But it doesn't have any notion of optional positional or required named parameters since that isn't meaningful for record objects since you either have the fields or you don't.

@Levi-Lesches
Copy link

Levi-Lesches commented Sep 29, 2022

But it doesn't have any notion of optional positional or required named parameters since that isn't meaningful for record objects since you either have the fields or you don't.

Won't they still be useful as a shorthand for null? Consider this example:

abstract class VersionCode { 
  String format() { /* format as semver string */ }
}

abstract class SemVer {
  VersionCode getVersion(int major, int minor, {required int? patch, required int? build});
}
abstract class OptionalBuild extends SemVer { 
  @override
  VersionCode getVersion(int major, int minor, {required int? patch, int? build});
}

abstract class MajorMinor extends SemVer {
  @override
  VersionCode getVersion(int major, int minor, {int? patch, int? build});
}

With records you could simplify the whole thing down into

typedef SemVer = (int major, int minor, {required int? patch, required int? build});
typedef OptionalBuild = (int major, int minor, {required int? patch, int? build});
typedef MajorMinor = (int major, int minor, {int? patch, int? build});
extension on SemVer {
  String format() { /* format as semver string */ }
}

And then

// 1.2.3+4
SemVer full = (1, 2, patch: 3, build: 4);
OptionalBuild noBuild = (1, 2, patch: 3);  // build = null
MajorMinor majorMinor = (1, 2);  // patch = build = null
print(full.format());  // v1.2.3+4
print(noBuild.format());  // v1.2.3
print(majorMinor.format());  // v1.2

Just like OptionalBuild.getVersion and MajorMinor.getVersion are valid overrides of SemVer.getVersion, noBuild and majorMinor should be considered SemVer records since share the same shape -- non-nullable major and minor, and nullable named patch and build fields.

@munificent munificent added records-later and removed records Issues related to records. labels Oct 21, 2022
@munificent munificent added records Issues related to records. and removed records-later labels Aug 28, 2023
@TekExplorer
Copy link

Why not let it be R Function<Record>? no real need to change how the return type is defined

@Levi-Lesches
Copy link

Wouldn't that conflict with a generic function?

typedef Adder<T> = T Function<T>(T a, T b);

@lrhn
Copy link
Member Author

lrhn commented Nov 7, 2023

If we wanted a type to abstract over function parameter signatures, we could just do introduce

R Function(..., {...})

to represent a supertype of any function which returns R.
The ... means "any parameters here".

That's probably better.

If we want to match it to an unknown record type, so that we know that the record can be spread into the argument list, without knowing the record shape or parameter list shape precisely... Then it's probably precisely the kind of thing we shouldn't do.

@TekExplorer
Copy link

Wouldn't that conflict with a generic function?

typedef Adder<T> = T Function<T>(T a, T b);

no, because you MUST have a parameter list if you define a return value

@TekExplorer
Copy link

If we wanted a type to abstract over function parameter signatures, we could just do introduce

R Function(..., {...})

to represent a supertype of any function which returns R. The ... means "any parameters here".

That's probably better.

If we want to match it to an unknown record type, so that we know that the record can be spread into the argument list, without knowing the record shape or parameter list shape precisely... Then it's probably precisely the kind of thing we shouldn't do.

Thats weird and doesnt make sense. The idea of having a record type for the function parameters is that we do have it.
it could be useful to pass in parameters

class Wrapper<R extends Record, T extends Something> {
  Wrapper(this._fn);
  final T Function(...R) _fn;
  List<T> repeat(...R args, {int repeat = 0}) sync* {
    for (final i = repeat; i >= 0; i--) {
       yield _fn(...args);
    }
  }
  
  Map<T> repeatMapped(...R args, {int repeat = 0}) {
     return {
         for (final e in repeat(...args, repeat: repeat))
            e.something: e,
      };
  }
}

suddenly we can change the return type, or even add parameters
Technically this exact example can be done with a VoidCallback parameter, but what if the class had more to it?
What if there were more wrappers?

@TekExplorer
Copy link

It could also be useful for storing parameters and comparing against parameters that are sent in later.
if they're ==, just return the cached value, as an example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
records Issues related to records.
Projects
None yet
Development

No branches or pull requests

8 participants