dart-lang / language

Design of the Dart language
Other
2.65k stars 201 forks source link

Support for the Optional type implicitly in the Dart language syntax #3731

Closed Ing-Brayan-Martinez closed 4 months ago

Ing-Brayan-Martinez commented 4 months ago

Introduction

Greetings team, today I bring you a much simpler proposal than the previous one and I see that it makes a lot of sense. I am going to limit myself to talking about the syntax. From this I tell you that in an informal meeting with my colleagues we decided to talk about the progress of the programming languages among other topics, and among all of us we noticed that the success of the optional type was interesting, many languages have incorporated some examples here:

The proposal

If we already know that the use of the optional type is widely consolidated because no longer making it part of the syntax, that would complete the innovation that Null Safety brought at the time. This is the best thing that the Dart language has, this makes it superior.

In any case, Swift, Apple's language has understood it well and they have already made it part of the language and it is my main source of inspiration, because in Swift we can do this:

let shortForm: Int? = Int("42")
let longForm: Optional<Int> = Int("42")

This means that a null integer Int? is equal to an optional integer Optional so the null symbol is also usable for optionals and can be described this way:

In Swift

let value1: Int? = Int("42")
let value2: Optional<Int> = Int("42")

let isEquals: Bool = value1 == value2 

print(isEquals) // print: true

In Dart

int? value1 = 42;
Optional<int> value2 = 42;

bool isEquals = value1 == value2 ;

print(isEquals) // print: true

In Dart there are several simplifications that are incorporated by default in the syntax of the language and I think that the optional type is completely compatible, being complementary to the idea of Null Safety, and personally it is the most beautiful and sexy thing there is in The last view was many years old and something surprised me. Some examples of syntax simplification in Dart are:

Asynchronous operations with the Future type

Future<Person> myFuntion() async {
   //you code ...
}

Streams on Dart

Stream<Person> myFuntion() async* {
   //you code ...
  yield Person();
}

Possible Optionals in Dart

Optional<int> myFuntion() {
  int? value = 42;
  return value;
}

Cascade notation

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

The pros

The cons

miyoyo commented 4 months ago

If Type? is equivalent to Optional<Type>, what's the point of Optional?

Any additional method you want to implement can be implemented with an extension on T?

lrhn commented 4 months ago

That's my reading too. If Optional<T> and T? has the same values, they are the same type.

With extensions, we can add methods to nullable types:

extension OptionalNullable<T extends Object> on T? {
  R? flatMap(R? Function(T) map) => switch (this) {var self? => map(self), _ => null};
}

With extension types, we can make Optional be a different type:

extension type const Optional<T extends Object>._(T? _) {
  const Optional.value(T value) : this._(value);
  const Optional.none() : this._(null);
  Optional<R> map<R extends Object>(R Function(T) convert) => _ == null ? null : Optional<R>._(convert(_));
  Optional<R> then<R extends Object>(Optional<R> Function(T) convert) => _ == null ? null : convert(_);
  T get value => _ ?? (throw StateError("No value"));
}

However, both of these suffer the same issue as T?, that it cannot represent an optional value that is null. That's usually the difference between nullable types and Optional, that Optional<int?>.value(null) is different from Optional<int?>.none().

If the Optional type has the same values as the nullable types, then that becomes impossible, and it misses the entire point of having an Optional type.

Ing-Brayan-Martinez commented 4 months ago

@miyoyo What's up friend, well it's time to explain better what I'm talking about, for that we must describe what Optional is because it exists

The optional type comes from the functional programming paradigm and what it seeks is null safety and referential transparency, the optional is a wrapper for a value and its premise is to ensure the value and provide a series of methods that allow managing the value that It has been wrapped under the concept of optional. Among the most common methods is isPresent() which means that the value is within the optional yes or no. I think it is better to explain it with a series of examples:

First let's see how to create an optional type using the library to see how to wrap a value

int? value = 42; //nullable int

Optional<int> optional = Optional.ofNullable(value);

if (optional.isPresent()) {
  print(optional.get());  //print: 42
}

Now let's see how to create an optional according to the proposal, the idea is to simplify the syntax and make it part of the language and not depend on an external library

Why is the latter possible because int? is the same as an Optional<int> only that the first is a short notation and the second is a long or complete notation

int? value = 42; //nullable int

Optional<int> optional = value;

if (optional.isPresent()) {
  print(optional.get());  //print: 42
}

Another example is the following error case because non-null int is not equal to an Optional<int>

int value = 42; // not null int

Optional<int> optional = value; // error

if (optional.isPresent()) {
  print(optional.get());  //print: 42
}

Any questions or suggestions to understand the proposal I will be waiting to explain what I am proposing

miyoyo commented 4 months ago

Again, Optional<T> here is useless, this code does the same thing, right now, in mainline dart:

extension OptionalType<T> on T? {
  bool isPresent() {
    return this is T;
  }

  T get() {
    return this!;
  }
}

int? nullableValue = 42;

if(nullableValue.isPresent()) {
  print(nullableValue.get());
}

Which, if you skip all of the added stuff, you can express as

int? nullableValue = 42;

if(nullableValue != null) {
  print(nullableValue);
}

The only thing this proposal adds is making optional an error if given a non-nullable without calling a special constructor, which is honestly pointless, as a type conversion of a non-nullable to a nullable is always safe.

Dart is not other languages, nor does it have to be.

Ing-Brayan-Martinez commented 4 months ago

@miyoyo I understand, interesting answer with the use of extensions a good tool Dart is a mature language, the point is that I am not only talking about isPresent() or get() to ask if it is null or not that does not make any sense I am talking about All operators present in Optional type please read what is in this link

Optional is a way to operate on null values to do all kinds of things other than just asking if it is null. No, I just illustrated basic examples to demonstrate what it would be like to use optional. Here I leave you some use cases so you can see beyond what you know. can do I am going to illustrate several advanced examples I will take my time

lrhn commented 4 months ago

Any questions or suggestions to understand the proposal I will be waiting to explain what I am proposing

Let me be concrete then!

The first message gave this example:

int? value1 = 42;
Optional<int> value2 = 42;
bool isEquals = value1 == value2;
print(isEquals) // print: true

Without any other, so far unmentioned, changes to the language, that means that the value of Optional<int> value2 = 42; is the integer 42. At least it's a value that the integer 42 considers equal to itself, which narrows it down to 42 and 42.0. The type Optional<int> does nothing. It has the same values as int? (I assume, if int? value1 = null; Optional<int> value2 = null; print(value1 == value2); also prints true.)

In the current language, the above means that Optional<T> is just an alias, typedef Optional<T> = T?;. We'd have to add a new kind of type to make it anything else and satisfy the equality above. That is why I'm asking what the properties of that type are, because we can take nothing for granted with a completely new kind of type.

(It's the details. It's always the details!)

Nothing prevents creating a class like:

sealed class Optional<T> {
  const Optional(T value) = Some<T>;
  const Optional.none() = None();
  bool get hasValue;
  Optional<R> map<R>(R Function(T) convert);
  Optional<R> then<R>(Optional<R> Function(T) convert);
  // Any other method you care about.
}
final class Some<T> implements Optional<T> {
  final T value;
  const Some(this.value);
  bool get hasValue => true;
  Some<R> map<R>(R Function(T) convert) => Some<R>(convert(value));
  Optional<R> then<R>(Optional<R> Function(T) compute) => compute(value);
}
final class None implements Optional<Never> {
  const None();
  bool get hasValue => false;
  None map<R>(R Function(T) convert) => const None();
  Optional<R> then<R>(Optional<R> Function(T) compute) => const None();
}

You won't have it being the same as nullability, and that's precisely the reason for having the type. We don't need another "nullable type", we already have those.

miyoyo commented 4 months ago

And if you want one more thing, here, java optionals. You can do it with extensions, yo ucan do it with classes, you can do it with whatever you want, but there is no reason to implement them at the langauge level

If you have a single case that cannot be solved by you writing a few lines of code, please provide it.

extension type const Optional<T extends Object>._(T? _internal) {
  Optional.of(T value): this._(value);
  Optional.ofNullable(T? value): this._(value); 
  Optional.empty(): this._(null);

  bool equals(Object? other) => this == other;

  Optional<T> filter(bool Function(T) predicate) {
    if(this._internal != null && predicate(this._internal)) {
      return this;
    } else {
      return Optional<T>.empty();
    }
  }

  Optional<U> flatMap<U extends Object>(U Function(T) mapper) {
    if(this._internal == null) return Optional<U>.empty();

    final result = mapper(this._internal);

    return Optional.of(result);
  }

  T get() {
    return this._internal!;
  }

  void ifPresent(void Function(T) action) {
    if(this._internal != null) action(this._internal);
  }

  void ifPresentOrElse(void Function(T) action, void Function() emptyAction) {
    if(this._internal != null) {
      action(this._internal);
    } else {
      emptyAction();
    }
  }

  Optional<T> or(Optional<T> Function() supplier) {
    if(this._internal == null) return supplier();
    return this;
  }

  T orElse(T value) {
    if(this._internal == null) return value;
    return this._internal;
  }

  T orElseGet(T Function() supplier) {
    if(this._internal == null) return supplier();
    return this._internal;
  }

  T orElseThrow([Exception Function()? exceptionSupplier]) {
    if(this._internal == null) {
      if(exceptionSupplier != null) throw exceptionSupplier();
      throw ArgumentError.notNull();
    }
    return this._internal;
  }

  Stream<T> stream() {
    if(this._internal == null) return Stream.empty();
    return Stream.value(this._internal); 
  }
}
Ing-Brayan-Martinez commented 4 months ago

@lrhn I understand, you are right, we have to pay attention to the details, let's do several examples to find any errors. I appreciate your answers. I am going to illustrate the examples:

Optional<int> value1 = 42;
Optional<int> value2 = 42;

print(vallue1 == value2) //print: true

// This is true because the operator == of the optional compares the wrapped value which is 42 and not the 
// hashCode of the wrapper. I don't know if I'm making myself understood.

// This is necessary to be able to do equivalent representation also in this way

int? value1 =  42;
int? vallue2 = 42;

// Here I am directly comparing the values

// Another way to look at it would be to do this, the idea is to make it simplified.

Optional<int> wrapped1 = 42;
Optional<int> wrapped2 = 42;

print(wrapped1.vallue == wrapped2.value) //print: true
print(wrapped1 == wrapped2) //print: true

print(wrapped1.vallue == value1) //print: true
print(wrapped2.vallue == value2) //print: true

The latter is to demonstrate the idea is to avoid that wrapped.vallue and have the == operator do all the work

miyoyo commented 4 months ago

Just add

class Some<T> extends Optional<T> {

 ... 

  bool operator==(Object? other) {
    if(other is Some<T> && other.value == this.value) return true;
    if(other is T && other == this.value) return true;
    return false;
 } 

  int get hashCode => value.hashCode;
} 

and

class None<T> extends Optional<T> {

 ... 

  bool operator==(Object? other) => other is None<T> || (other is T? && other == null);

  int get hashCode => None<T>.hashCode;
} 

And you're done. Note that this only applies if you don't do type aliases.

Ing-Brayan-Martinez commented 4 months ago

@miyoyo y @lrhn For now I am going to try to do it this way using extentions, the idea is that it comes from the factory, that is, within the Dart SDK as part of the standard libraries and that it is a recommended practice for developers, I appreciate all the support, help me implement this solution to the entire community by including it in the Dart SDK

miyoyo commented 4 months ago

help me implement this solution to the entire community by including it in the Dart SDK

No.

I don't want to be harsh, but I feel like I have to, given your multiple issues which have been closed for being off topic.

It seems that you are thinking that "Optional in Java is good, Dart doesn't have optional, therefore adding Optional to Dart makes it better."

This is a fundamental misunderstanding of the reason as to why optional even exists in the first place in Java.

The reason why it exists in Java, is that Java does not have non nullable types. Dart, however, does have this feature, making this entire thing have zero value in Dart.

Not a single thing suggested here provides an use case that is not either doable with a simple condition, or is already supported by the Dart type system.

I may be getting out of line here, but please stay away from the issues of this repository unless you truly understand what you're asking for.

Ing-Brayan-Martinez commented 4 months ago

@miyoyo I understand, the point is that Optional is not exclusive to Java, it is a functional programming concept, all of this is complementary to the null safety that we already have, it is just providing some additional properties to make algorithms more flexible and have the opportunity to make more declarative code. I thought that implementing this would be more complicated but it turned out to be simpler, the idea is to extend the functionality of null safety and if it is to compare us to other languages, I think that this functionality makes Dart look more like Swift than Java, that is, I am Inspired by Swift, use the Java documentation to speed up the explanation and not Java, it is very complicated, that is why I prefer Swift.

Help me with this, you know it is useful, you could do a survey and ask the community, at this point the proposal is clear and you can consult with the community, obviously if there is rejection there is nothing to do

miyoyo commented 4 months ago

you know it is useful

My point is that it is not. I'm no longer going to respond here.

Ing-Brayan-Martinez commented 4 months ago

I understand at least read what is in this link here it explains how Swift already has it implemented and it is Sexy grateful

lrhn commented 4 months ago

I may be getting out of line here

You are. Ideas are welcome, even if they aren't fully baked feature designs.

Such ideas do have a much greater chance of being adopted if they can be expressed in terms of problems and solutions.

This thread is a proposal for a feature, but without being concrete about which problems it solves.

That makes it very hard to evaluate the feature on its own terms. It's hard to decide whether it's a good solution, without knowing what it's a solution to.

And equally hard to say whether the same problem can be solved adequately by existing language features.

I do agree that it feels like "this is a great feature in other languages, so Dart should have it too", without considering whether Dart has the same problems that the feature is solving for those other languages. Which just comes back to not being explicit about which problems it's trying to solve.

So, what problem are we trying to solve here?

Ing-Brayan-Martinez commented 4 months ago

@lrhn You are right and I am going to put it that way.

Description of the problem

When we think about pure functional programming, this paradigm becomes a utopia because it proposes a scenario where the algorithms work because nulls do not exist and everything works perfectly because the situation arises that the code does not interact with the external world, everything is immutable, reality is that most algorithms and systems need to talk to the outside world

Languages like Haskell have extraordinary abstractions to be able to represent console prints, for these cases where we have to interact with the outside world such as accessing the database, making an http request, or trying to open a file, in all of these cases we have the risk. that the object we are requiring exists or does not exist, for example, creating a method that tries to recover a File could be represented as an optional file Optional<File> because that file could not exist

Currently to represent this we only have the null operator ? so that file can only be represented as a null File? The only operation I can do is ask if that file is null or not and in an ideal case use the operator ?? to replace it with a default file and nothing more

The point is that the options to operate a null value are very limited, such as verifying its existence or replacing it with another value and nothing more.

Another problem is that you must be constantly checking if the value of File? Because you have the risk that it could be null because you are operating directly with the value, this forces you to use the operator ?. "Conditional member access" for risk-free access and the operator ! "Non-null assertion operator" to make a nullable value compatible with something non-null

// current version only with child safety, which is very good as is

File? getFile() {
  //you code ...
}

void main() {
 File? miFile = getFile();

if (miFile != null) {
  String? path = miFile.getPhat();

 print(path ?? 'Not Value');
} else {
 print('not file');
}

Solution Description

The solution is to have more options to be able to manage null values. For this to be possible, we must make them equivalent to the optional type. This allows us to have a series of operators to manage that value that may be null because it comes from the outside world, because we can wrap it under the optional concept and operate on that value according to the methods provided by the optional type

The fact that the null value is wrapped implies that there is no risk in accessing it because you have control. This allows you to isolate possible points of failure because any null value is wrapped under the concept of optional and you never have to directly access the null value. It can be checked to ensure that there are no risks.

So the example I talked about a moment ago could look like this:

// alternative version using the optional type as a complement to the null safety which is already excellent

Optional<File> getFile() {
  //you code ...
}

void main() {
 Optional<File> miFile = getFile();

 Optional<String> path = miFile.map((file) =>  file.getPhat() );

 print(path.orElse('Not Value'));
}
// alternative version using the simplified optional type

File? getFile() {
  //you code ...
}

void main() {
 File? miFile = getFile();

 String? path = miFile.map((file) =>  file.getPhat() );

 print(path ?? 'Not Value');
}

Some options that are added would be:

The number of uses of the optional type is very wide, another solution that indirectly comes with the optional type is that the stream can more safely manage nulls since they will be the same as the otionals, it can be operated on that wrapped null value

Another important point of this solution is that we are indirectly improving the support of Functional Programming within the Dart language, this paradigm is widely used lately, Dart already has the support of the operators of the Iterable and Stream interfaces that allow us to support Functional programming currently the optioanal type is a complementary functionality to what we already have

Ing-Brayan-Martinez commented 4 months ago

Good morning, another important point that I have not explained is how the optional type affects the existing null-safe operators. To demonstrate that they are fully compatible, I am going to make some examples.

With type optional using map()

Optional<File> getFile() {
  //you code ...
}

void main() {
 File? miFile = getFile();

 String? path = miFile.map((file) =>  file.getPhat() );

 print(path ?? 'Not Value');
}

With type optional using ?.

Optional<File> getFile() {
  //you code ...
}

void main() {
 File? miFile = getFile();

 String? path = miFile?.getPhat()

 print(path ?? 'Not Value');
}

with type optional using orElse()

void main() {
 String? name = 'Joel'

 print(name.orElse('Not Value'));
}

with the type optional using ??

void main() {
 String? name = 'Joel'

 print(name ?? 'Not Value');
}

with type optional using get()

void main() {
 String? name = 'Joel'

 String notNullName = name.get();

 print(notNullName);
}

with type optional using !

void main() {
 String? name = 'Joel'

 String notNullName = name!;

 print(notNullName);
}
lrhn commented 4 months ago

The point is that the options to operate a null value are very limited, such as verifying its existence or replacing it with another value and nothing more.

While that's true, it may be that you can solve all actual problems using a few powerful operations. It doesn't tell us about any actual problems that are hard to solve using the existing functionality.

The examples of how new methods will do precisely the same as existing operators is not a good selling point. It proves that we don't need those methods. We already have those problems solved. And better. (A value.orElse(computeAlternative()) would compute the alternative even if it isn't needed, whereas value ?? computeAlternative() would not.)

An actual problem, which those operators do not solve optimally, could be:

Today that requires:

ResultType? result = value != null ? computation(value) : null;

That's not as convenient as what one can do with ?. or ??.

A solution to that could be a simple extension function:

extension ApplyTo<T> on T {
  R applyTo<R>(R Function(T) computation) => computation(this);
}

which you can then use as:

ResultType? result = value?.applyTo(computation);

This has the advantage that the null check happens as early as possible.

Another option is to use patterns:

ResultType? result = switch (value) {
  var value? => computation(value),
  _ => null,
};

This is fairly readable and direct, and it doesn't require calling extra functions.

The null aware operators are very general and powerful (because they are short-circuiting, they prevent further computations that a function call wouldn't). They cover the main cases: do something if value is null (??) and do something if value is not null (?.). There are other operations, too: The ??= default assignment and the ...? null-aware spread, and patterns!

And if one wants an optional type that is not like T?, it's also easy to write that using extension types:

extension type const Opt<T>._(({T value})? _) {
  const Opt(T value) : this._((value: value));
  const Opt.none() : this._(null);
  const Opt.from(T? value) : this._(value == null ? null : (value: value));
  Opt<R> map<R>(R Function(T) convert) => 
      Opt<R>._(_ == null ? null : (value: convert(_.value)));
  Opt<R> then<R>(Opt<R> Function(T) convert, {Opt<R> Function()? ifNone}) =>
      _ == null ? (ifNone?.call() as Opt<R>) : convert(_.value);
}

extension FlatOpt<T> on Opt<Opt<T>> {
  Opt<T> get flatten => _?.value as Opt<T>;
}

extension type const Some<T>._(({T value}) _) implements Opt<T> {
  const Some(T value) : this._((value: value));
  T get value => _.value;
}

extension type const None<T>._(Null _) implements Opt<T> {
  const None() : this._(null);
}

void main() {
  var maybe = DateTime.now().millisecondsSinceEpoch.isEven;
  var value = maybe ? Opt<int>(21) : Opt<int>.none();
  value = value.map((x) => x * 2);
  switch (value) {
    // Use switch to check if there is a value, and extract it.
    case Some(:var value):
      print("Value was: $value");
    case _:
      print("No value");
  }
}

With the existing null-aware operators and patterns, I haven't come up with many things that I can't write, and a simple extension method like applyTo above solves all the inconveniences I've actually had.

Which is why I still don't think that the proposal here to add an extra type, no matter how it's defined as long as it's not just typedef Optional<T> = T?;, is solving any problem that doesn't have a solution already.

Ing-Brayan-Martinez commented 4 months ago

I think you have not understood me, I will go directly to the problem, I will demonstrate it to you with examples.

If we have a non-null String, it is safe to access any method of the String class, for example toUpperCase(), see it in the code

void main() {
  String notNullName = 'Joel';

  print(notNullName.toUpperCase());
}

Now if we make that same String nullable even with this change, the IDE allows me to access the String methods, which makes it even risky because accessing the toUpperCase() method could be done using the operator ?. Well, even so, the IDE still suggests that I can access it and the risk is still present, see it in code

void main() {
  String? name = 'Joel';

  print(name.toUpperCase()); // Access error, The IDE should not suggest this
}

Using the operator ?.

void main() {
  String? name = 'Joel';

  print(name?.toUpperCase()); // Possible access but still ambiguous
}

How we solve this situation, taking the nullable string and putting it inside a container called Optional. When doing it this way, it is not possible to access the toUpperCase() method because it is referring to the Optional object and not to the String directly. This indicates that there is no risk to be able to use toUpperCase(), you must extract the String from the optional to be able to access all the methods of the String

Only methods or operators compatible with the optional can be used and only if the value is nullable to ensure that it is present and can use the internal value that is wrapped inside the Optional, see it in code.

Using optional notation

void main() {
  Optional<String> name = 'Joel';

  // Error: Because you cannot directly access toUpperCase() because you are referring to the Optional wrapper, to access
  // toUpperCase() you must extract the String making sure that it is present without any risk

  print(name.toUpperCase()); //Access error, The IDE should not suggest this

  print(name?.toUpperCase()!); // Correct access, because operators are used that automatically ensure access

  print(name?.toUpperCase() ?? 'Not Value'); // Correct access, with default value
}

Using simplified optional notation

void main() {
  Optional<String> otherName = 'Joel';

  String? name = otherName; // Optional<String> equivalent String?

  // Error: Because you cannot directly access toUpperCase() because you are referring to the Optional wrapper, to access
  // toUpperCase() you must extract the String making sure that it is present without any risk

  print(name.toUpperCase()); // Access error, The IDE should not suggest this

  print(name?.toUpperCase()!); // Correct access, because operators are used that automatically ensure access

  print(name?.toUpperCase() ?? 'Not Value'); // Correct access, with default value
}

Using extended optional notation

void main() {
  Optional<String> otherName = 'Joel';

  String? name = otherName; // Optional<String> equivalent String?

  // Error: Because you cannot directly access toUpperCase() because you are referring to the Optional wrapper, to access
  // toUpperCase() you must extract the String making sure that it is present without any risk

  print(name.toUpperCase()); //Access error, The IDE should not suggest this

  print( name.map((str) => str.toUpperCase()).get() ); // Correct access

  print( name.map((str) => str.toUpperCase()).orElse('Not Value') ); // Correct access, with default value
}

The change is to be able to wrap nullable values to isolate them and access them safely and avoid checking whether the null value exists or not because Otional will do it automatically, wrapping nullable values this Dart cannot currently do.

tatumizer commented 4 months ago
void main() {
  String? name = 'Joel';

  print(name.toUpperCase()); // Access error, The IDE should not suggest this
}

The analyzer doesn't "suggest" an error because it can prove that the variable is not null (in this context). Any error raised by the compiler here would be a false alarm. If the context is not as trivial, the analyzer raises an error:

String? getName()=>"Joel"; 
main() {
  var name=getName();
  name.toUpperCase(); // error: The method "toUpperCase" cannot be unconditionally invoked etc..
}

The same logic is applied to all potentially null-unsafe operations.

Ing-Brayan-Martinez commented 4 months ago

@tatumizer You have exactly understood it in a context as trivial as can be seen according to the example that you have indicated to me, according to the current version of null safety, I think that the analyzer should suggest the following because right now String? is understood as a String | Null see it in the code:

String? getName() => "Joel";  // error: Can't use String? in this context use String

void main() {
  var name = getName();
  name.toUpperCase(); 
}

If we include the concept of optional the analyzer will have clues as to what a null container is and a real value. If we apply this logic we will be able to illustrate this example

String? getName() => "Joel";   // Remember that String? will be equal to Optional<String>

void main() {
  var name = getName();
}

In the previous example, the analyzer does not complain, suggesting that you pass String? to a String because no method has been called if we try to call any method of the string we have 2 paths

1 Suggest a warning that you can pass from String? to a String to access the methods of the String class 2 Suggest an error for trying to access incorrectly. It should be noted that this should not happen because the IDE should at no time suggest accessing any method of the String because it is an Optional<String> and can only be accessed with methods of the Optional type or null operators.

String? getName() => "Joel";   // Remember that String? will be equal to Optional<String>

void main() {
  var name = getName();
  name.toUpperCase();  // error: It is not possible to access toUpperCase() because it is not part of Optional<string>
}
tatumizer commented 4 months ago

Why do you think this has to be an error?

String? getName() => "Joel";  // error: Can't use String? in this context use String

Maybe this is just a stub for a future program that will sometimes return null? And in any case, every string is also a "string or null". For the same reason as "Joel is a human or a cat" is a true statement. The programmer might have many reasons to declare a return type as String? What if I write

Animal a = Horse();

Is it an error? I tried swift, and the line var i: Int? = 42 compiles just fine. If you think this must be an error in dart, why not to start with submitting a similar request to swift (the language you cited as an inspiration). :-)

EDIT: swift has some features that look rather strange. E.g., when you try to print a nullable variable, the compiler issues a warning (not an error, the value gets printed anyway in runtime). What's wrong with printing? I speculate that swift doesn't want users to ever see a word "null" ("nil"), as if the word itself was anathema. Dart doesn't have such an attitude.

Ing-Brayan-Martinez commented 4 months ago

@tatumizer I'm just going to tell you that the behavior of the analyzer must respond to the code statements that have been explicitly placed String? and the entire context indicates that there are no nulls, there must be something that indicates that this statement is out of place

Perhaps you are right and one error is too many to represent this case. We can invest in a warning. Is it obvious that in the example that you indicated the String? It is out of place as a good programmer that does not work for me but it is something that the Optional can fix effectively because everything will be explicit

String? getName() => "Joel";   // warning: Can't use String? in this context use String

void main() {
  var name = getName();
  name.toUpperCase(); 
}

It is evident that this is possible to identify and the Optionals fix the problem effectively

tatumizer commented 4 months ago

Why isn't this flagged in swift then? No error, no warning:

func getName() -> String? {
    "Joel" // no error, no warning
}

I even tried

func getName() -> Optional<String> {
    "Joel" // no error, no warning
}

Can you point to one language where the concept of Optional is defined in such a manner that the above program gets flagged?

Ing-Brayan-Martinez commented 4 months ago

@tatumizer It is obvious that in Swift this problem does not exist because they already have the Optionals implemented and if you see that it is not necessary to make that warning, do not add it, I do not see any problem, that is not an obstacle to adding the Optionals, the main idea is to include them the rest is your decision

I have already said it on repeated occasions, the Optionals effectively fix the root problem.

tatumizer commented 4 months ago

Then we are back to square one. What is the root problem then? Please show a single example where the alleged deficiency of dart's nullable types manifests itself and leads to unsafe programming (please provide just a short snippet of code).

julemand101 commented 4 months ago

@Ing-Brayan-Martinez You seem to be confused about how Dart have chosen to solve the null-safety problem and how they have made solutions to prevent developers from making useless null checks. I think this missing knowledge is part of the reason why you think adding a Optional type are going to solve some issue since you are not aware that the issues you are trying to solve with Optional are in fact, not an issue in Dart.

I very much suggest reading about null-safety in Dart (especially the part about "Flow analysis"): https://dart.dev/null-safety/understanding-null-safety

I'm just going to tell you that the behavior of the analyzer must respond to the code statements that have been explicitly placed String? and the entire context indicates that there are no nulls, there must be something that indicates that this statement is out of place

You are wrong here. The Dart analyzer have already implemented lot of analysis to make sure developers only need to handle null-safety issues where it matters. If you follow the development of the language, you will see there are often progress in making the analyzer even smarter when it comes to this determination of null-safety to remove even further checks the developer needs to do.

So it is a feature of the Dart language that the analyzer will not just take the word of the developer as the complete truth. If the analyzer knows better, it will do what it have analyzed to be the true type of a given variable.

And this is seen, by a lot of developers, as a great feature of the language since we can write our programs a lot smarter since we can write code based on how the program are going to run and don't have this feeling of adding null-checks where we, as a developer, already can clearly see it is not needed.

If you really suggests that all the currently implemented parts of the Dart null-safety should be removed in favor of just always use the type the developer are suggesting, then it would be a massive breaking change to the language without any benefits other than weakening the null-safety of the language just to be able to provide another solution to the problem. The Optional class....

And that is, in my opinion, a pointless endeavor.

Ing-Brayan-Martinez commented 4 months ago

@julemand101 I never talked about eliminating anything, only about incorporating the Optionals to enhance what we already had, I have already said that null security is very good, and that the optionals will help optimize the experience

Why do you have to choose? Yes all the features are necessary and I just wanted to show that it is compatible with what we already have, it is only to optimize the experience we already have

julemand101 commented 4 months ago

@Ing-Brayan-Martinez

@julemand101 I never talked about eliminating anything

Please read your own comment:

I'm just going to tell you that the behavior of the analyzer must respond to the code statements that have been explicitly placed String? and the entire context indicates that there are no nulls, there must be something that indicates that this statement is out of place

Perhaps you are right and one error is too many to represent this case. We can invest in a warning. Is it obvious that in the example that you indicated the String? It is out of place as a good programmer that does not work for me but it is something that the Optional can fix effectively because everything will be explicit

You are here suggesting removing part of the null-safety feature we already have in the language by adding warnings to places that does not need warnings because of the current null-safety implementation can see null-checks would be pointless.

Ing-Brayan-Martinez commented 4 months ago

@julemand101 It was a mistake on my part. The point is to incorporate the optionals in a complementary way is what I have said from the beginning, and to optimize the experience that is all

The optionals will rely on the parser to function and the parser will find clues to work better thanks to the optionals to identify the real values and the null containers both need each other.

The rest is your decision

julemand101 commented 4 months ago

@Ing-Brayan-Martinez

The point is to incorporate the optionals in a complementary way is what I have said from the beginning, and to optimize the experience that is all

Examples please that shows a benefit over the current way of doing it in Dart. Your previous attempts on providing such examples have been with "mistakes on your part" so we need examples without such mistakes.

And the reason I then tell you to do some research about the current null-safety implementation in Dart, is because your mistakes have a root cause of misunderstanding how null-safety works in Dart already and the reasons why it works as it does.

The optionals will rely on the parser to function and the parser will find clues to work better thanks to the optionals to identify the real values and the null containers both need each other.

Which is what the current null-safety implementation already does and which is why you are being asked to provide concrete examples that shows an actual benefit if we choose your proposal.

The rest is your decision

I suggest spend more time making your messages a lot clearer if you think the issue here is us misinterpret your messages.

Ing-Brayan-Martinez commented 4 months ago

Creo que no me has entendido, iré directo al problema, te lo demostraré con ejemplos.

Si tenemos un no nulo String, es seguro acceder a cualquier método del Stringclase, por ejemplo toUpperCase(), verlo en el código

void main() {
  String notNullName = 'Joel';

  print(notNullName.toUpperCase());
}

Ahora si hacemos eso mismo Stringanulable incluso con este cambio, el IDE me permite acceder al Stringmétodos, lo que lo hace aún más riesgoso porque acceder a la toUpperCase()El método se puede realizar utilizando el operador. ?.Bueno, aun así, el IDE todavía sugiere que puedo acceder a él y el riesgo sigue presente, véalo en código.

void main() {
  String? name = 'Joel';

  print(name.toUpperCase()); // Access error, The IDE should not suggest this
}

Usando el operador ?.

void main() {
  String? name = 'Joel';

  print(name?.toUpperCase()); // Possible access but still ambiguous
}

Cómo solucionamos esta situación, tomando la cadena que acepta valores NULL y colocándola dentro de un contenedor llamado Opcional. Al hacerlo de esta manera, no es posible acceder al método toUpperCase() porque se refiere al objeto Opcional y no al String directamente. Esto indica que no hay riesgo al poder usar toUpperCase(), debes extraer el String del opcional para poder acceder a todos los métodos del String

Solo se pueden usar métodos u operadores compatibles con el opcional y solo si el valor admite valores NULL para garantizar que esté presente y pueda usar el valor interno que está incluido dentro del Opcional, véalo en el código.

Usando notación opcional

void main() {
  Optional<String> name = 'Joel';

  // Error: Because you cannot directly access toUpperCase() because you are referring to the Optional wrapper, to access
  // toUpperCase() you must extract the String making sure that it is present without any risk

  print(name.toUpperCase()); //Access error, The IDE should not suggest this

  print(name?.toUpperCase()!); // Correct access, because operators are used that automatically ensure access

  print(name?.toUpperCase() ?? 'Not Value'); // Correct access, with default value
}

Usando notación opcional simplificada

void main() {
  Optional<String> otherName = 'Joel';

  String? name = otherName; // Optional<String> equivalent String?

  // Error: Because you cannot directly access toUpperCase() because you are referring to the Optional wrapper, to access
  // toUpperCase() you must extract the String making sure that it is present without any risk

  print(name.toUpperCase()); // Access error, The IDE should not suggest this

  print(name?.toUpperCase()!); // Correct access, because operators are used that automatically ensure access

  print(name?.toUpperCase() ?? 'Not Value'); // Correct access, with default value
}

Usando notación opcional extendida

void main() {
  Optional<String> otherName = 'Joel';

  String? name = otherName; // Optional<String> equivalent String?

  // Error: Because you cannot directly access toUpperCase() because you are referring to the Optional wrapper, to access
  // toUpperCase() you must extract the String making sure that it is present without any risk

  print(name.toUpperCase()); //Access error, The IDE should not suggest this

  print( name.map((str) => str.toUpperCase()).get() ); // Correct access

  print( name.map((str) => str.toUpperCase()).orElse('Not Value') ); // Correct access, with default value
}

El cambio es poder envolver valores anulables para aislarlos y acceder a ellos de forma segura y evitar verificar si el valor nulo existe o no porque Otional lo hará automáticamente, envolviendo valores anulables que Dart no puede hacer actualmente.

I already explained it here, I'm going to make more examples.

julemand101 commented 4 months ago

@Ing-Brayan-Martinez Please. That post is the one that contains your "mistake on my part" by having this example:

void main() {
  String? name = 'Joel';

  print(name.toUpperCase()); // Access error, The IDE should not suggest this
}

We have just talked about this and where you agreed that was a mistake and this error should not be added.

Ing-Brayan-Martinez commented 4 months ago

@julemand101 If we define the current form of null-safe and null value is defined this way


void main() {
  String? name = 'Joel'; // String | null
}

If we describe this we can identify that the reference to the object may or may not be present because it is a union type String | Null, being a double type, is difficult to manage because you have to ask if it is null manually, and to access it safely you have to use the ?. operator.

My proposal focuses on optimizing references to not have a union type String | Null, it is best to have a single type to reference, that is, type Optional<String>. What advantages does this have?

The advantage is to eliminate the ambiguity of a double type like String | Null to have a unique type Optional<String>

Another advantage is that the null value is wrapped and is impossible to access directly because the Optional type keeps it isolated from the outside world, preventing any errors.

Another advantage is the application of all the methods provided by the Optional type, extending the functionality to have more options to operate on a null value.

We can represent it this way


void main() {
  String? name = 'Joel'; // Optional<String>
}

This is the main point, the rest of what was talked about is the compatibility of the Optional type with the rest of the language features

I don't understand why it's so difficult to explain or understand. I don't understand what I'm doing wrong to express a clear and concise idea.

julemand101 commented 4 months ago

I don't understand why it's so difficult to explain or understand. I don't understand what I'm doing wrong to express a clear and concise idea.

@Ing-Brayan-Martinez You are being asked to provide examples that shows an actual problem being solved by your solution in a way that is better than the way we solve the same issue in current version of Dart.

..., is difficult to manage because you have to ask if it is null manually, and to access it safely you have to use the ?. operator.

That is how we are suppose to deal with null-safety issues. The type-system forces you to be aware of potential null-values and write your code in such a way that it does not end up getting a NullPointerException. There are also other ways to access the null-value but in common, the solutions is about proving to the analyzer that you are taking care of potential null-values or you tell the analyzer to trust you and have a potential NullPointerException at runtime.

The tools already provided by Dart to handle null-safety issues are already great. So we ask you to provide examples that clearly shows where the current null-safety are failing and where your proposal are providing a great solution to those situations.

Ing-Brayan-Martinez commented 4 months ago

@julemand101 That's why you're not understanding me, the current version has no flaws, and it's very good, I've already said it.

The only thing I can point out is that it has some small limitations to apply functional programming, limitations that do not affect the functioning of the language.

My proposal seeks to extend functionality and better support functional programming and optimize security to nulls and nothing more.

My proposals are something simple to understand, it is minimalist, it is necessary to "see Rome burn in flames" to justify a change that brings positive things to the community

lrhn commented 4 months ago

My proposals are something simple to understand

It may be simple to understand for yourself, but you have, so far, not managed to express the proposal in a way that is simple to understand for other people. I'm unclear about what is concretely being proposed, other than

I think you might be suggesting more warnings, fx when assigning from String to String?, but that's not something that requires a new type.

All in all, I still do not see any concrete example where a new type makes any difference. Nothing suggested here can't be done with just T?.

What problem are you trying to solve?

My proposal focuses on optimizing references to not have a union type String | Null, it is best to have a single type to reference, that is, type Optional<String>. What advantages does this have?

Do tell!

The advantage is to eliminate the ambiguity of a double type like String | Null to have a unique type Optional<String>

Which ambiguity? The type String? is well-defined, well supported by numerous "null-aware" language features, and sound. The language promises that you cannot possibly use a value of that type as a String without first ensuring that it's not null (which then usually promotes a variable to type String for as long as we can know that it still has that type, another language feature).

Another advantage is that the null value is wrapped and is impossible to access directly because the Optional type keeps it isolated from the outside world, preventing any errors.

There is nothing you can do with a value with static type String? that you can't do with something typed as Object?. The null and/or String is fully "wrapped". You can't assign String? to String or to Null, because it might not be either. You have to check it before you can use it for anything.

Another advantage is the application of all the methods provided by the Optional type, extending the functionality to have more options to operate on a null value.

All of these can be extension types on T? today, no new type needed. Most of those methods would be inferior to the short-circuit behavior of ?. and ??. It really is only map/flatMap/then/applyTo, etc., where you call a function with the value as argument, if the value is not null. We have no syntax for that. And value?.applyTo(function) can do that efficiently using a single extension method.

The only thing I can point out is that it has some small limitations to apply functional programming,

Anything other than what applyTo can fix?

Ing-Brayan-Martinez commented 4 months ago

Friend, I was thinking about leaving the thread, I'm going to make one last attempt to explain to you, I'm going to ask you several questions to verify if I have managed to communicate the proposal correctly and if we both understand the same idea.

What is functional programming for you, what value can it bring to the Dart language?

What is the Optional type for you, give me a definition of what you understand as the Optional type?

When I say that the type Optional<String> is equal to String? allowing to provide additional methods that help manage nulls, using functional programming, to complement what we already have. Tell me what you understand about this. Explain it to me?

When I say that the Optional type allows you to wrap a null value, isolating it from the outside world to avoid errors, and that it can only be accessed through special methods of the optional type, using functional programming, and not directly as is currently done, and that Is the difference with the current null safety compared to the optional one, what do you understand?

When I say that my proposal seeks to expand functionality and better support functional programming to optimize null security, being complementary to what we already have, what do you understand?

When I say that null safety currently has small limitations for applying functional programming, do you understand?

Help me answer these questions, so I can know exactly why I have not been able to explain myself correctly, and from that context answer specifically the doubts you have.

And if you think it is pertinent, you could do the same with me, you could give me a series of questions about the doubts you have with this to see if we reach the end of this.

julemand101 commented 4 months ago

And if you think it is pertinent, you could do the same with me, you could give me a series of questions about the doubts you have with this to see if we reach the end of this.

@Ing-Brayan-Martinez I am not sure if your translation software have some issues. But please do notice that lrhn did in fact ask you "a series of questions about the doubts you have with this to see if we reach the end of this" which you have yet to answer.

Please provide answers without asking further questions. It is your job right now to answer the questions you already got and answer them with clear explanation and shortest examples as possible. And please don't point at previous attempts on explaining yourself. We have already tried to read your previous posts, which is the reason why we still have questions.

We don't need more confusion that we already have.

Ing-Brayan-Martinez commented 4 months ago

What problem are you trying to solve?

Implement functional programming in null safety, with the help of the Optional type, expanding this characteristic

Which ambiguity? The type String? is well-defined, well supported by numerous "null-aware" language features, and sound. The language promises that you cannot possibly use a value of that type as a String without first ensuring that it's not null (which then usually promotes a variable to type String for as long as we can know that it still has that type, another language feature).

The current definition is String? is equal String | Null is a double type because according to its definition it can have a value or it can be null and that is correct. The point is that it can be improved by using Optional, why now String? will be Optional<String> and will be completely isolated because it is wrapped inside the optional

There is nothing you can do with a value with static type String? that you can't do with something typed as Object?. The null and/or String is fully "wrapped". You can't assign String? to String or to Null, because it might not be either. You have to check it before you can use it for anything.

The values are not wrapped in the current version, the idea is to wrap it inside the Optional and have the null checks done automatically is the advantage of functional programming

All of these can be extension types on T? today, no new type needed. Most of those methods would be inferior to the short-circuit behavior of ?. and ??. It really is only map/flatMap/then/applyTo, etc., where you call a function with the value as argument, if the value is not null. We have no syntax for that. And value?.applyTo(function) can do that efficiently using a single extension method.

Is it obvious that all optionals type methods can be added to the type T? through extentions and with an alias typedef Optional<T> = T?;That would be a quick solution for me to simulate the Optional type, but it would not be an optional as such because it is not wrapped, it is just a simulation

Ing-Brayan-Martinez commented 4 months ago

Actually what I was always talking about is Monad Maybe which is present in Haskell, a pure functional programming language.

Talking about pure functional programming is a complicated activity, I prefer to use more simple terminology such as Optional, which has been accepted for its minimalist vision and is used in multiple languages.

I remember that from 2014 to 2019 there was a lot of talk about implementing programming to work in a minimalist way in the developer community and since then all languages have included features to support this paradigm.

If implementing the optional ones at this time brings the situation that breaks the ecosystem or delays the development of the Dart language you can make the decision to postpone or not include this feature you have the power

Dart, from its first versions, was already designed to be a language that supported functional programming by default, which is why the Streams are part of the syntax of the Dart language. It was greatly inspired by all the advances that happened between 2014 and 2019. The Optionals are in tune with the original identity of the language

I think we have reached the end of all this, greetings

lrhn commented 4 months ago

To be honest, if I was designing an object oriented language from scratch, I would probably not include a null value, and would have something like an Optional type instead.

But we're not starting from scratch, and Dart has null, and has designed the language around that, adding sound nullable types, type promotion on != null, and numerous null-aware operations and patterns.

That doesn't mean that people isn't added Option classes themselves. They have, precisely because it can provide one thing that nullability doesn't: the ability to distinguish an absent value from a present value of null.

Everything else, references to functional programming included, is something you can do with nullable types too.

You can think of T? as Dart's Optional<T> type. It's already there. Everything you've asked for is built in, or a simple extension function away, and with built-in short-circuiting that a class method cannot compete with. It already solves every problem you have mentioned, it encapsulates the values and requires you to call a method to distinguish a value from a non-value (== is a method, if you don't look too close). Or at least use an operation.

The only thing or doesn't do is allowing null to also be a value, but you haven't mentioned that as a problem. In most cases it isn't a problem, if you build your API appropriately.

(When you say "functional programming" I read side-effect free and compositional. I don't consider fx Stream to be functional, it's object oriented with objects and methods. Using higher order functions, aka callbacks, isn't the same as functional programming in the Haskell or Scheme sense. There is more to that. So saying that an option type would make Dart better support functional programming is questionable, and if still want to see the actual problems it solves better than the current nullable types.)

Ing-Brayan-Martinez commented 4 months ago

@lrhn Good day friend, if I tell you that Null is necessary, I think this is easier to solve than the type T? It is totally necessary because somewhere you must be able to assign the null value that can be verified.

We know that the Dart language has type inference, the idea is to use inference to be able to implement the optional. I will show you some examples

Then declare a function that returns null

String? getName() => null; 

The type inference must interpret is that this null is actually an Optional.empty() because the null value is assigned and wrapped inside the optional, that is, null still exists.

String? getName() => null;  // It's actually an Optional.empty()

String? getName() => Optional.empty(); 

Another way to look at it is like this

Optional<String> getName() => null;  // It's actually an Optional.empty()

Optional<String> getName() => Optional.empty();

For the Optional type to exist, the T? type must also exist because it will be used as the internal value. Optional<T?>could be represented in this way. The internal value of the optional must be an Object | Null so it can work

So when we represent a null value, in reality that value still exists, it is just that it is wrapped inside the optional and can be represented as Optional.empty() if you can't see it yet it would be like an Optional<null> totally isolated from the outside world

mraleph commented 4 months ago

I think this back and forth discussion is not a productive way to spend time because involved parties are just speaking past each other.

You can read blog post to see why Dart went with the current design around nullable types.

Changing T? to Optional<T> in a breaking way is impossible at this point in time.

Aliasing Optional<T> as T? in a non breaking way is of no significant value to the developers.

Adding Optional<T> in addition to T? also does not seem to be of any significant use, but you are free to build some variation of this through extension types and personally assess the benefit.