-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Unsound type check: it compiles but fails at runtime #51680
Comments
Implementing it as an extension method works because of the difference between static type arguments and runtime type arguments. Inside of Result, T and E reflect the type argument at construction, not declaration. |
The basic issue here is dynamically checked covariance. The following version of the basic declarations type checks with // `--enable-experiment=variance`.
enum ResultType { ok, error }
class Result<out T, inout 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<out T> extends Result<T, Never> {
final T value;
ResultOk(this.value) : super(ResultType.ok);
}
class ResultErr<inout E> extends Result<Never, E> {
final E error;
const ResultErr(this.error) : super(ResultType.error);
} The reason why In the invocation that fails at run time, the function literal You can use the variance feature to look into the details about where you are using dynamically checked covariance in the example. The first thing is that PS: Vote for dart-lang/language#524 in order to help adding support for static checking in this kind of situation. ;-) |
Thanks to the both of you for the explanations. I hope the variance proposal will go through soon, it's awful when the compiler doesn't tell you it will for sure crash your program at some point. |
Thanks! By the way, I think the abstract class Result<out T, inout E> {
const Result();
Result<T2, E> andThen<T2>(Result<T2, E> Function(T) f);
Result<T2, E> map<T2>(T2 Function(T) f);
}
class ResultOk<out T, inout E> implements Result<T, E> {
final T value;
ResultOk(this.value);
Result<T2, E> andThen<T2>(Result<T2, E> Function(T) f) => f(value);
Result<T2, E> map<T2>(T2 Function(T) f) => ResultOk(f(value));
}
class ResultErr<inout E> extends Result<Never, E> {
final E error;
const ResultErr(this.error);
Result<T2, E> andThen<T2>(Result<T2, E> Function(Never) f) => this;
Result<T2, E> map<T2>(T2 Function(Never) f) => this;
}
void main() {
final ok = ResultOk<int, String>(10);
final res = ok.andThen((n) {
if (n.isEven) return ResultOk(n.toString());
return ResultErr("not even");
});
print('Result type: ${res.runtimeType}');
} The |
@eernstg Thanks for your suggestion, I did try it already, but couldn't find a good solution for another operation I need: Result<T, E2> mapErr<E2>(
Result<T, E2> Function(E) f
); I couldn't find a way for ResultOk to work it out 🤔 |
I think you'll have to preserve both type arguments in order to allow for abstract class Result<inout T, inout E> {
const Result();
Result<T2, E> andThen<T2>(Result<T2, E> Function(T) f);
Result<T2, E> map<T2>(T2 Function(T) f);
Result<T, E2> mapErr<E2>(Result<T, E2> Function(E) f);
}
class ResultOk<inout T, inout E> implements Result<T, E> {
final T value;
ResultOk(this.value);
Result<T2, E> andThen<T2>(Result<T2, E> Function(T) f) => f(value);
Result<T2, E> map<T2>(T2 Function(T) f) => ResultOk(f(value));
Result<T, E2> mapErr<E2>(Result<T, E2> Function(E) f) => ResultOk(value);
}
class ResultErr<inout T, inout E> extends Result<T, E> {
final E error;
const ResultErr(this.error);
Result<T2, E> andThen<T2>(Result<T2, E> Function(T) f) => ResultErr(error);
Result<T2, E> map<T2>(T2 Function(T) f) => ResultErr(error);
Result<T, E2> mapErr<E2>(Result<T, E2> Function(E) f) => f(error);
}
void main() {
final ok = ResultOk<int, String>(10);
final res = ok.andThen<String>((n) {
if (n.isEven) return ResultOk(n.toString());
return ResultErr("not even");
});
print('Result type: ${res.runtimeType}');
} |
I was so focused on returning Thank you! |
@eernstg sorry to further bother you, but I am having trouble enabling the experimental flag on Android Studio. Do you, by any chance, have any knowledge of it? I did set it in
This is what I have on the analysis file: analyzer:
exclude: [build/**]
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
enable-experiment:
- variance |
I don't think it is possible to enable experiments using analysis_options.yaml, it's given on the command line, using |
By the way, you can always create a good approximation of invariance (if you're willing to pay a little bit extra at run time, and willing to dive into less-than-obvious error messages wherever the invariance is violated): abstract class _Result<T, E, Invariance extends _Inv<T, E>> {
const _Result();
Result<T2, E> andThen<T2>(Result<T2, E> Function(T) f);
Result<T2, E> map<T2>(T2 Function(T) f);
Result<T, E2> mapErr<E2>(Result<T, E2> Function(E) f);
}
class _ResultOk<T, E, Invariance extends _Inv<T, E>> implements
_Result<T, E, Invariance> {
final T value;
_ResultOk(this.value);
Result<T2, E> andThen<T2>(Result<T2, E> Function(T) f) => f(value);
Result<T2, E> map<T2>(T2 Function(T) f) => ResultOk(f(value));
Result<T, E2> mapErr<E2>(Result<T, E2> Function(E) f) => ResultOk(value);
}
class _ResultErr<T, E, Invariance extends _Inv<T, E>> implements
_Result<T, E, Invariance> {
final E error;
const _ResultErr(this.error);
Result<T2, E> andThen<T2>(Result<T2, E> Function(T) f) => ResultErr(error);
Result<T2, E> map<T2>(T2 Function(T) f) => ResultErr(error);
Result<T, E2> mapErr<E2>(Result<T, E2> Function(E) f) => f(error);
}
typedef _Inv<T1, T2> = (T1, T2) Function(T1, T2);
typedef Result<T, E> = _Result<T, E, _Inv<T, E>>;
typedef ResultOk<T, E> = _ResultOk<T, E, _Inv<T, E>>;
typedef ResultErr<T, E> = _ResultErr<T, E, _Inv<T, E>>; |
I think I'll close this issue: The dynamically checked covariance is known, and this issue plus one possible fix is covered by dart-lang/language#524. |
Ok, I'm fine with that, however is the linked issue the one you intended? I can't see the connection between this and 524 |
Fixed the link! |
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:
We can then write a quick test for it:
Everything will compile, but once we run it, it fails with this error message:
The simpler
map
function, and similar, works as expected:Interestingly enough, defining
andThen
as a function works as expected: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!
The text was updated successfully, but these errors were encountered: