Skip to content

Support cloning functions #4281

Open
Open
@rrousselGit

Description

@rrousselGit

One common programing pattern is to have a reusable utility which wraps a function to perform logic before/after and to transform the result.

A typical example is a logger middleware. One might write:

T Function() logged<T>(T Function() cb) {
  return () {
    print('Before');
    final value = cb();
    print('After $value');
    return value;
  };
}

Then used as:

class Example {
  late final method = logged(() {
    return 42;
  }); 
}

The problem is:
Our logged util is bound to the T Function() prototype. It does not accept any other Function variants.
If we want a Example.method which takes parameters, we have to write a new logged ; such as loggedUnary / loggedBinary / ...

This is a common issue. For example we have Zone.current.bindCallback vs Zone.current.bindUnaryCallback vs Zone.current.bindBinaryCallback ...

Proposal: Make all Functions have a compose method, and add a TypedFunction<Return, Arg> interface

The idea is that all functions would implement a new interface defined as:

// A new primitive, similar to Invocation but typed.
abstract class ArgumentList {}

abstract class TypedFunction<ReturnT, ArgsT extends ArgumentList> {
  TypedFunction<NewReturnT, ArgsT> compose<NewReturnT>(
    NewReturnT Function(
      ReturnT Function(ArgsT args),
      ArgT args,
    ) cb,
  );

 // Not variadic arguments. I'm just using ... because I don't know what other syntax would fit
  ReturnT call(...ArgsT args);
}

Consider:

String nullary() => 0;
int unary(int a) => a;
int complex(int a, {required String b, double c = 42, bool? d}) => 42;

Those functions would implement TypedFunction as followed:

TypedFunction<String, _()> a = nullary;
TypedFunction<int, _(int)> b = unary;
TypedFunction<int, _(int, {required String b, double c = 42, bool? d})> b = complex;

Note:
The second generic type in this snippet isn't a Record! It's a new type that represents a subclass of ArgumentList.

Based on those interfaces, we can do:

final complex2 = complex.compose((call, args) {
  print('Before $args');
  final result = call(args);
  print('After $result');
  return result;
})

complex2(1, b: 'b', c: 2, d: false);

This would log:

Before _(1, b: 'b', c: 2, d: false)
After 42

Example 1: Logging

Using this new feature, we could implement an all purpose logged utility:

TypedFunction<ResT, ArgsT> logged<ResT, ArgsT extends ArgumentList>(
  TypedFunction<RestT, ArgsT> cb,
) {
  return cb.compose((call, args) {
    print('Before $args');
    final result = call(args);
    print('After $result');
    return result;
  });
}

This could then be used as

class Example {
  late final method = logged(() {
    return 42;
  });

  late final method2 = logged((int arg, {required String value}) {
    ...
  }
}

void main() {
  final example = Example();
  example.method();
  example.method2(1, value: 'value');
}

Example 2: Wrapping functions in a Result<T>

Consider:

sealed class Result<T> {}
class ResultData<T> implements Result<T> {}
class ResultError<T> implements Result<T> {}

We could implement a generic guard utility:

TypedFunction<Result<T>, ArgsT> guard<T, ArgsT extends ArgumentList>(
  TypedFunction<T, ArgsT> cb,
) {
  return cb.compose((call, args) {
    try {
      final result = call(args);
      return ResultData(result);
    } catch (err) {
      return ResultError(result);
    }
  });
}

This could then be used as

class Example {
  late final method = guard(() {
    return 42;
  });

  late final method2 = guard((int arg, {required String value}) {
    return 42;
  }
}

void main() {
  final example = Example();
  Result<int> a = example.method();
  Result<int> b = example.method2(1, value: 'value');
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions