LazyCollection.map()


(Aaron Bohannon) #1

Does the code below have a well-defined behavior?

struct Nope: ErrorType {}

func f(i: Int) throws -> Int {
  guard i < 5 else { throw Nope() }
  return i
}

do {
  let _ = try Array(0 ..< 10).lazy.map(f)
  print("lazy")
} catch (let e) {
  print(e)
}


(Dmitri Gribenko) #2

It invokes the eager map() that is available on Array.lazy.
Array.lazy is a collection, so it has an eager map() from the
Collection protocol. The lazy map() does not accept a throwing
closure, so it does not match and the type checker chooses the eager
one.

Arguably, in non-generic context this code should not type check.

Dmitri

···

On Tue, Jun 28, 2016 at 3:37 PM, Aaron Bohannon via swift-users <swift-users@swift.org> wrote:

Does the code below have a well-defined behavior?

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Aaron Bohannon) #3

AH! I wasn't familiar enough with the LazyCollectionType protocol to
realize that its map() function had a different signature.

So, presumably, if I pass a non-throwing closure, the compiler will choose
the lazy map(). However, it's not immediately obvious to me why it would
be chosen. Is it because the LazyCollectionType version of map() "shadows"
the CollectionType whenever either one could be chosen (since
LazyCollectionType extends CollectionType)? Or is it because the function
with the more restrictive argument type is always chosen when more than one
version of the function could match?

···

On Tue, Jun 28, 2016 at 5:38 PM, Dmitri Gribenko <gribozavr@gmail.com> wrote:

On Tue, Jun 28, 2016 at 3:37 PM, Aaron Bohannon via swift-users > <swift-users@swift.org> wrote:
> Does the code below have a well-defined behavior?

It invokes the eager map() that is available on Array.lazy.
Array.lazy is a collection, so it has an eager map() from the
Collection protocol. The lazy map() does not accept a throwing
closure, so it does not match and the type checker chooses the eager
one.

Arguably, in non-generic context this code should not type check.

Dmitri

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Dmitri Gribenko) #4

LazyCollectionType.map() does not shadow Collection.map(), but the
type checker prefers the lazy one because LazyCollectionType refines
Collection. Basically, it prefers the more refined one, but the other
one is still an option.

Dmitri

···

On Tue, Jun 28, 2016 at 6:07 PM, Aaron Bohannon <aaron678@gmail.com> wrote:

AH! I wasn't familiar enough with the LazyCollectionType protocol to
realize that its map() function had a different signature.

So, presumably, if I pass a non-throwing closure, the compiler will choose
the lazy map(). However, it's not immediately obvious to me why it would be
chosen. Is it because the LazyCollectionType version of map() "shadows" the
CollectionType whenever either one could be chosen (since LazyCollectionType
extends CollectionType)? Or is it because the function with the more
restrictive argument type is always chosen when more than one version of the
function could match?

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/


(Aaron Bohannon) #5

I didn't really understand your answer, so I figured that I'd just
experiment until I could see the pattern. Unfortunately, I found very few
signs of any underlying pattern -- and I didn't even get a chance to
experiment with generics. All I can see is behavior that is frighteningly
inexplicable. I've tried to explain my confusion in the inline comments
below. (FYI: I'm still using Swift 2.2 at the moment... if that matters.)

- Aaron

// Trivial class hierarchy for the examples
class C {}
class D: C {}
class E: D {}

// Trivial protocol hierarchy for the examples
protocol P {
  var a: Bool { get }
}
protocol Q: P {
  var b: Bool { get }
}
class R: Q {
  let a = true
  let b = true
}

// Base protocol with some default implementations
protocol BaseProtocol {}
extension BaseProtocol {
  // f1 has a more permissive parameter type than f2
  func f1(x: Int??) -> String { return "Base" }
  func f2(x: Int?) -> String { return "Base" }

  // g1 has a more permissive parameter type than g2
  func g1(x: C) -> String { return "Base" }
  func g2(x: D) -> String { return "Base" }

  // h1 has a more permissive parameter type than h2
  func h1(x: P) -> String { return "Base" }
  func h2(x: Q) -> String { return "Base" }

  // two functions with incomparable but overlapping parameter types
  func v1(x: (Int?, Int)) -> String { return "Base" }
  func v2(x: (Int, Int?)) -> String { return "Base" }

  // two functions with incomparable but overlapping parameter types
  func w1(x: () -> Int?) -> String { return "Base" }
  func w2(x: () throws -> Int) -> String { return "Base" }
}

// Derived protocol with some default implementations
protocol DerivedProtocol: BaseProtocol {}
extension DerivedProtocol {
  // f2 has a more permissive parameter type than f1
  func f1(x: Int?) -> String { return "Derived" }
  func f2(x: Int??) -> String { return "Derived" }

  // g2 has a more permissive parameter type than g1
  func g1(x: D) -> String { return "Derived" }
  func g2(x: C) -> String { return "Derived" }

  // h2 has a more permissive parameter type than h1
  func h1(x: Q) -> String { return "Derived" }
  func h2(x: P) -> String { return "Derived" }

  // two functions with incomparable but overlapping parameter types
  func v1(x: (Int, Int?)) -> String { return "Derived" }
  func v2(x: (Int?, Int)) -> String { return "Derived" }

  // two functions with incomparable but overlapping parameter types
  func w1(x: () throws -> Int) -> String { return "Derived" }
  func w2(x: () -> Int?) -> String { return "Derived" }
}

class Z: DerivedProtocol {}

// First we check the functions whose parameters have built-in types:

print(Z().f1(7)) // prints "Derived"
print(Z().f2(7)) // prints "Base"

// The outcome seems to demonstrate that the location of the function's
definition within the protocol hierarchy is irrelevant. As in Java, it
appears that all accessible, applicable functions are being given equal
consideration, and then the function whose signature has a more specific
parameter type is chosen. That makes sense to me.

// Next we check functions whose parameters have class types:

print(Z().g1(E())) // prints "Derived"
print(Z().g2(E())) // prints "Derived"

// Surprisingly, the outcome here appears to contradict the previous one: a
function with a less specific type is given preference. This must be due
to the fact that it is defined in the derived protocol rather than the base
protocol. Does that mean we should call this case "overriding" while the
previous one was "overloading"? But I can't understand why there could be
any value in treating user-defined types different from built-in types.
Maybe the behavior here is a bug?

// Now we check the same functions again using an argument with a less
specific type that happens to be identical to the formal parameter type of
one of the functions:

print(Z().g1(D())) // prints "Derived"
print(Z().g2(D())) // prints "Base"

// And the outcome is different than before! So, for user-defined types,
is there a special rule that only applies when the formal and actual
parameters have precisely the same type? Of course, we wouldn't need to
call this a special case if the behavior in the second example had been the
same as the first, which lends support to the idea that the second example
is actually illustrating a bug... but we're not done testing yet.

// What happens with functions whose formal parameters have protocol types?

print(Z().h1(R())) // prints "Derived"
print(Z().h2(R())) // prints "Derived"

print(Z().h1(R() as Q)) // prints "Derived"
print(Z().h2(R() as Q)) // prints "Derived"

// Well it appears they are handled like class types rather than built-in
types -- except that the special-case behavior that was observed when the
class types are identical is not coming into effect here. So, maybe the
second example was not a bug? Are we making a deliberate decision to
sometimes give the function definition in the derived class precedence over
the first? But what exactly would we mean by "sometimes"? And why would
having that sometimes-different behavior be a good thing. If we have three
different elementary overload resolution schemes for the different kinds of
types, what's going to happen when we try to combine those types? Or
combine overloading with other language features? Like generics, etc.?
Yikes!

// I also decided to test the behavior for a pair of functions, both of
which can be applied to the argument but neither of which can match the
argument's type in a more specific way than the other. I suspected this
might somehow trigger a type error, but it didn't. Instead, the definition
in the derived protocol was given preference:

print(Z().v1((0, 0))) // prints "Derived"
print(Z().v2((0, 0))) // prints "Derived"

// I suppose that, if a type error isn't going to be raised when neither
function is a better fit than the other, then it makes sense to use the
definition in the derived class as a default. Unfortunately, my final
example demonstrates that we can't even count on that much:

print(Z().w1({ return 7 })) // prints "Derived"
print(Z().w2({ return 7 })) // prints "Base"

// Given that these last two examples exhibit different behavior, the
compiler really ought to define them both as type errors. However, I'm
even more worried by the diverging behavior of the earlier examples.

···

On Tue, Jun 28, 2016 at 7:09 PM, Dmitri Gribenko <gribozavr@gmail.com> wrote:

On Tue, Jun 28, 2016 at 6:07 PM, Aaron Bohannon <aaron678@gmail.com> > wrote:
> AH! I wasn't familiar enough with the LazyCollectionType protocol to
> realize that its map() function had a different signature.
>
> So, presumably, if I pass a non-throwing closure, the compiler will
choose
> the lazy map(). However, it's not immediately obvious to me why it
would be
> chosen. Is it because the LazyCollectionType version of map() "shadows"
the
> CollectionType whenever either one could be chosen (since
LazyCollectionType
> extends CollectionType)? Or is it because the function with the more
> restrictive argument type is always chosen when more than one version of
the
> function could match?

LazyCollectionType.map() does not shadow Collection.map(), but the
type checker prefers the lazy one because LazyCollectionType refines
Collection. Basically, it prefers the more refined one, but the other
one is still an option.

Dmitri

--
main(i,j){for(i=2;;i++){for(j=2;j<i;j++){if(!(i%j)){j=0;break;}}if
(j){printf("%d\n",i);}}} /*Dmitri Gribenko <gribozavr@gmail.com>*/