[RFC] Undefined Type

So I had the idea of introducing a new undefined type/value with the purpose of allowing the default values of a method to be invoked, even if a value was provided. For example:

class Example
  def self.from_range(range : Range, message : String = "Not in range")
    new range.begin, range.end, message
  end
  
  def initialize(@min : Int32, @max : Int32, @message : String = "Not in range"); end
end
 
Foo.from_range (1..10)

In order for default message property to be used when using the from_range constructor, it has to be duplicated on both definitions. WIth this proposal you would be able to something like:

class Example
  def self.from_range(range : Range, message : String = undefined)
    new range.begin, range.end, message
  end
  
  def initialize(@min : Int32, @max : Int32, @message : String = "Not in range"); end
end
 
Foo.from_range (1..10)

When not providing a message to from_range the value of the message argument is undefined; thus when the actual constructor sees undefined it knows to treat that value as if it wasn’t provided at all, resulting in the message being Not in range. However if you do provide a message to from_range the argument would be the passed in message, resulting it that value being used.

Currently this is kinda possible by doing something like:

class Example
  def self.from_range(range : Range, message : String? = nil)
    new range.begin, range.end, message
  end
  
  def initialize(@min : Int32, @max : Int32, message : String? = nil)
    @message = message || "Not in range"  
  end
end
 
Foo.from_range (1..10)

However I don’t like that the message argument needs to be made nilable when in fact it technically isn’t and is only nilable for this use case. I’d be down for other solutions as well, like maybe if nil is passed as an argument that isn’t nilable and has a default value, just use the default? Not a real big fan of that as it could probably lead to some bugs/surprising behavior that would have normally been caught.

Thoughts?

I think Brian mentioned this to me in the past, or we had the same idea. It’s a good idea. The thing is that we have to weight how hard it’s to implement this, how intuitive it is, vs existing solutions that are already possible. For example you could extract the default value into a constant. Or… a bit of duplication is sometimes fine.

For example, what happens when you use message when it wasn’t passed? I guess you get an error the same way if message wasn’t actually passed?

Another way to implement this is by providing two overloads, though I guess that’s more duplication.

1 Like

Yea for sure. I wouldn’t say its high priority, definitely an after 1.0 thing. I recently learned that TypeScript does this and quite liked the concept.

Yea, my thinking was that if you pass undefined to something that doesn’t have a default, the compiler could know and error saying like that argument can be undefined but does not have a default value.

I guess the semantics of undefined would essentially just be like:

  1. Allows using the default value of an argument
  2. Compile time error if used on an argument without a default
  3. Compile time error when using any method on it

I thought of this but it can quickly get messy if you have like more than a handful of optional arguments.

Can you post a link to this feature in Typescript?

Sorry, apparently it’s a JavaScript feature.

The last section in https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters#Description

image

Just with TypeScript do you get some type safety around it: https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABAWxAG1gBzQTwBQCGAXImCMgEYCmATgDSIUlmW2IC8iAjAJSIDeAKEQjENKlBA0kBRACpGAbkEBfQYIgIAznDRUAdGjgBzPKgwxs+AKwMATDx7LNYHXsMmz6LLjzXHztq6Bkam5j42DOAAJlTAMGBU0QGCQA. I.e. if you remove the default value, notice it errors because undefined isn’t compatible with number. But explicitly passing undefined when it has a default is fine.

That’s a bit different, right? You pass undefined at the call site, not at the method definition.

It works similarly when using undefined as the default value. However you do have to add undefined type to the allowed types:

function multiply2(a: number, b: number | undefined = undefined) {
    return multiply(a, b);
}

multiply2(5) # => 5

undefined is just JavaScript’s nil (more or less). This is not specific to function arguments.

How do you figure? They behave differently. E.x. the second screenshot I linked. undefined causes the default value be used, null does not.

Although they are similar, the semantics between the two are slightly different.

1 Like

I think the nilable solution is a perfectly fine solution to the problem. This is not worth the enormous cognitive complexity it adds to the language where you have to learn and keep in mind the difference between nil and undefined and learn that undefined is not what you may know from other languages.

Let’s please start to strive to keep the language semantics simple or to simplify it further. Adding feature after feature does the opposite.

10 Likes

I agree with @jhass here, one of Ruby/Crystal’s great strengths imo is that we don’t add nuance to nil. It’s really nice to know that every thing I could add to my program is either not defined or is defined as something in the language. And the language allows exactly one flavor of ‘defined to hold nothing’ => nil.

I’ve never enjoyed that a variable can be defined to be undefined in javascript

a = undefined

Modern javascript has tried to help devs with this issue by introducing the idea of “nullish” to include both undefined and null but … it’s a patch over a problem that bites again and again over in that world.

3 Likes