1. 19
  1.  

  2. 17

    You can pass a List<int> to a function that wants a List<int?>.

    It’s worth pointing out that this is not always sound. For example, this results in a runtime type error:

    addNull(List<int?> xs) {
      xs.add(null);
    }
    
    main() {
      List<int> blah = [];
      addNull(blah);
    }
    

    Of course, it’s valid to decide that this is an acceptable price to pay. TypeScript does this (with no runtime errors since the types basically just disappear), and I guess Dart does it as well.

    1. 8

      Isn’t this basically type covariance vs type contravariance? Some languages (notably Scala) allow to explicitly specify which is desirable.

      1. 4

        If the List type constructor was contravariant instead (i.e. List<int?> was a subtype of List<int>), then you would have analogous problem with the types swapped:

        oops(List<int> xs) {
          int x = xs[0];
          // do something with x
        }
        
        main() {
          List<int?> blah = [null];
          oops(blah);
        }
        

        So it doesn’t seem that this dichotomy is relevant here.

        1. 14

          An immutable list is covariant in its type parameter. A mutable list is invariant, neither covariant or contravariant. The proper solution is for List<A> and List<B> to be unrelated types in the sub typing relation.

          1. 3

            It is relevant. The covariance/contravariance of the type depends on whether it’s in the “input” position or the “output” position. But generally speaking, List is neither contravariant nor covariant.

            Kotlin gets this correct with its in and out qualifiers on generic parameters.

        2. 1

          “Valid” is a measured tradeoff between convenience and potential for errors. Because, it can cause runtime errors.

          function addString<a>(lst: (a | string)[]) {
              lst.push("oh no")
          }
          
          let xs: number[] = [1, 2, 3]
          addString(xs)
          
          // straightforward function contract, returns a number
          function sum(lst: number[]): number {
              return lst.reduce((a, b) => a + b)
          }
          
          console.log(sum([2, "bar"]))  // <- this produces a type error
          console.log(sum(xs))          // <- this doesn't, but it doesn't return a number
          
          // will this behave as it looks like it will?
          for (let i = 0; i <= sum(xs); i++) {
              console.log("hi")
          }
          

          The alternative is explicit variance specification, or seriously weakening subtyping relations.

        3. 8

          TL;DR: If your language has used null pervasively, nullable types look like an easier upgrade than option types.


          If we do a lookup and get back null, it could mean either …

          I’m in shock that they copied Java’s design mistake here. WHY?

          1. 5

            I thought the article was pretty good, other than glossing over the covariance/contravariance as @quasi_qua_quasi pointed out.

            These days, I pretty strongly favor the Option approach specifically because of the nesting as mentioned in the article. You don’t always need that, but when you do, it pretty much forces you to swim upstream of the language if it chose the nullable approach.

            Also, Swift’s approach is kind of a hybrid. I think it’s technically more like an Option, but it has a ton of syntax sugar so that it feels exactly like the nullable type approach except when you want to do an explicit switch statement.

            1. 2

              Friendly reminder, you can reference other users here with an @-prefix.

              1. 2

                Thank you. I wasn’t aware.

            2. 5

              Nullable types exist in the static type system, but the runtime representation of values uses the underlying type. If you have a “nullable 3”, at runtime it’s just the number 3. If you have an absent value of some nullable type, at runtime you just have the solitary magic value null.

              I don’t see how this is possible; you need some out-of-band value to represent null. Magic doesn’t exist, it’s just something operating by rules you can’t see.

              1. 3

                Would this work fine if everything is boxed? Without knowing the runtime of Dart, I wouldn’t be surprised if that’s true.

                1. 1

                  …heck you’re right, that’s the obvious answer.

                2. 2

                  Here’s an example of how it works in two languages I use:

                  TypeScript:

                  let foo: number | null
                  foo = 3
                  console.log(typeof foo) // "number"
                  foo = null
                  console.log(typeof foo) // "object"
                  console.log(typeof null) // "object"
                  

                  TypeScript null values report their own type. A value has the same runtime type no matter where it’s used. (This is a bit trivialized by the fact that TypeScript vanishes into JavaScript before runtime.)

                  Swift:

                    1> var foo: Optional<Int>
                  foo: Int? = 0
                    2> foo = 3 // Shorthand for Optional<Int>.some(3)
                    3> print(Mirror(reflecting: foo))
                  Mirror for Optional<Int>
                    4> foo = nil // Shorthand for Optional<Int>.none
                    5> print(Mirror(reflecting: foo)) 
                  Mirror for Optional<Int>
                    6> print(nil)
                  error: repl.swift:6:7: error: 'nil' is not compatible with expected argument type 'Any'
                  

                  Swift null values are a case of the generic type Optional. The runtime type of an Optional is the same, regardless of whether it’s the .some case or the .none case. Here it’s either .some and contains an int, or it’s .none and does not contain anything. That is to say, nil is just a keyword that refers to a case of a certain standard library type. You can’t even print it without a concrete Optional type, because it is not itself a value. The out of band value you’re referring to is the bit of the Optional that indicates which case it is.

                  An interesting effect here is that Optional<Int>.none and Optional<SomethingElse>.none are both usually expressed by nil and then inferred in context, but they are distinct values, can’t be assigned to a variable of the other type, and are unlikely to be the same allocated size. Compare with TypeScript, where you can take a number | null, compare to see if it is null, and assign the result to a receiver of type string | null. Swift has no such value that can be copied between distinct concrete types of Optional.