Search code examples
dartgenericsfunctional-programming

How can we ensure that the combination of functions `(x) => Context(y)` and `(y) => Context(z)` is correct in Dart during the syntax analysis phase?


How can we ensure that the combination of functions (x) => Context(y) and (y) => Context(z) is correct in Dart during the syntax analysis phase?

I'm attempting to develop code that allows the combination of monadic functions, which return values wrapped in a context. I have managed to create a function that exhibits satisfactory behavior:

O2 Function(T1)
      combine<T1, T2, T3, O1 extends MetaState<T2>, O2 extends MetaState<T3>>(
          O1 Function(T1) f1, O2 Function(T2) f2) {
    extractor(x) => x.extract();
    return (T1 x) => f2(extractor(f1(x)));

But for this I need to write something like this:

var p = combine(
        (int x) => Value<int>(x + 5), 
        (int x) => Value<String>('$x'));

var p2 = combine(p, (int x) => Value<String>('$x')); // error in the IDE

And the goal is in the following syntax:

var p = ImmutablePipeline()
      .bind((int x) => Value<int>(x + 5))
      .bind((int x) => Value<String>('$x'))
      .bind((int x) => Value<String>('$x')); // but not error in the IDE

And here's an example of a simple class that performs the task but does not perform the correctness checking during the syntax analysis phase:

class ImmutablePipeline<T extends MetaState> {
  final Function? pipeline;

  const ImmutablePipeline({Function? initial}) : pipeline = initial;

  T2 extractor<T2>(MetaState<T2> x) => x.extract();

  ImmutablePipeline<T> bind<S>(T Function(S) f) {
    if (pipeline == null) {
      return ImmutablePipeline<T>(initial: f);
    } else {
      T newf(x) => f(extractor(pipeline!(x)));
      return ImmutablePipeline<T>(initial: newf);
    }
  }

  MetaState produce(T s) {
    return pipeline!(s.extract());
  }
}

I have tried many variations, but I have not achieved the desired result. Generics in Dart seemed very complex and non-trivial to me in this case. It is also important that the usage of the class is not overly complicated and that the function composition is generic across different contexts. Perhaps someone knows a better way to solve this problem? Or a solution? Any ideas?


Solution

  • I'm not entirely sure what all the classes you use are doing.

    If you want to abstract over the monad itself ("generic over different contexts" suggests so), you likely cannot. That would require higher-order generics, which Dart doesn't have. If you just want to have different instantiations of the same monad, it should be doable.

    Simply doing a monadic combine is "easy":

    MetaState<T3> Function(T1) combine<T1, T2, T3>(
      MetaState<T3> Function(T2) second, Metastate<T2> Function(T1) first) => 
         (T1 v1) => second(first(v1).extract());
    

    That works, and so does something like:

    abstract class EmptyPipeline<S> {
      ImmutablePipeline<S, T> bind<T>(MetaState<T> Function(S) initial) =>
         ImmutablePipeline<S, T>(initial);
      MetaState<Never> produce(S s) => throw StateError("Empty pipeline");
    }
    
    class ImmutablePipeline<S, T> {
      final MetaState<T> Function(S) pipeline;
    
      const ImmutablePipeline(this.pipeline);
    
      ImmutablePipeline<S, R> bind<R>(MetaState<R> Function(T) f) =>
         ImmutablePipeline<S, R>((S s) => f(pipeline(s).extract()));
    
      MetaState<T> produce(MetaState<S> s) => pipeline(s.extract());
    }
    

    if that is what you want it to do.