Skip to content

Unsound type check: it compiles but fails at runtime #51680

Closed
@iazel

Description

@iazel

Dart SDK version: 2.19.2 (stable) (Tue Feb 7 18:37:17 2023 +0000) on "linux_x64"


UPDATE: implementing it as an extension function works as expected. Quite odd.


Hello,

I've implemented the usual Result<T, E> type. In case you are unfamiliar with it, it is just a type that carries the result of a computation that could fail. You can think of it as an exception moved into data.

Anyway, I've defined it as such:

enum ResultType { ok, error }

class Result<T, E> {
  final ResultType type;

  const Result(this.type);
 
  Result<T2, E> andThen<T2>(Result<T2, E> Function(T data) f) {
    switch (type) {
      case ResultType.ok:
        return f((this as ResultOk<T>).value);
      case ResultType.error:
        return this as Result<T2, E>;
    }
  }
}

class ResultOk<T> extends Result<T, Never> {
  final T value;

  ResultOk(this.value) : super(ResultType.ok);
}

class ResultErr<E> extends Result<Never, E> {
  final E error;

  const ResultErr(this.error) : super(ResultType.error);
}

We can then write a quick test for it:

test('happy case', () {
  final Result<int, String> ok = ResultOk(10);
  final Result<String, String> res = ok.andThen((n) {
    if (n % 2 == 0) {
      return ResultOk(n.toString());
    } else {
      return const ResultErr("not even");
    }
  });

  expect(res, isA<ResultOk>());
});

Everything will compile, but once we run it, it fails with this error message:

type '(int) => Result<String, String>' is not a subtype of type '(int) => Result<String, Never>' of 'f'

The simpler map function, and similar, works as expected:

Result<T2, E> map<T2>(T2 Function(T) f) {
  switch (type) {
    case ResultType.ok:
      return ResultOk(f(this as ResultOk<T>).value));
    case ResultType.error:
      return this as Result<T2, E>;
  }
}

Interestingly enough, defining andThen as a function works as expected:

Result<T2, E> andThen<T, T2, E>(
    Result<T, E> r,
    Result<T2, E> Function(T) f,
) {
  switch (r.type) {
    case ResultType.ok:
      return f((r as ResultOk<T>).value);
    case ResultType.error:
      return r as Result<T2, E>;
  }
}

test('happy case', () {
  final Result<int, String> ok = ResultOk(10);
  final Result<String, String> res = andThen(ok, (int n) {
    if (n % 2 == 0) {
      return ResultOk(n.toString());
    } else {
      return const ResultErr("not even");
    }
  });

  expect(res, isA<ResultOk>());
});

Notice that this time n in not properly inferred, hence we have to manually specify it, but at least this is caught by both analyzer and compiler.

Hope the issue is clear,
Thanks for your work and support!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions