Skip to content

Instantly share code, notes, and snippets.

@tmspzz
Last active June 2, 2017 19:26
Show Gist options
  • Save tmspzz/53f9568617654c38a219dd4a8353d935 to your computer and use it in GitHub Desktop.
Save tmspzz/53f9568617654c38a219dd4a8353d935 to your computer and use it in GitHub Desktop.
Add Tuple unsplatting - Removed by SE-0110

Add Tuple unsplatting

As a side effect of SE-0110 tuple unsplating was removed from the language.

While this claims to make tooling like the type checker faster, it deals quite a blow to expressivity.

Filterting dictionaries is just an example and maybe not the best. However I hope the point gets across. Let's compare to other languages.

Examples of pattern matching in closure parameters:

Swift

let eighteenOrMore = ["Tom" : 33, "Rebecca" : 17, "Siri" : 5].filter { arg in
  let (_, age) = arg // Awkward :| No tuple unsplatting
  return age >= 18
}

// OR

// Still No tuple unsplatting
let eighteenOrMore = ["Tom" : 33, "Rebecca" : 17, "Siri" : 5].filter { $0.1 >= 18 } 

// OR

// Somewhat better
let eighteenOrMore = ["Tom" : 33, "Rebecca" : 17, "Siri" : 5].filter {(arg: (name: String, age: Int)) in arg.age >= 18 }

Kotlin

mapOf("Tom" to 33, "Rebecca" to 17, "Siri" to 5).filter({ (_, age) -> age >= 18 })

Python 2.7

{name: age for name, age in {'Tom': 33, 'Rebecca': 17, 'Siri': 5}.iteritems() if age >= 18 }

Ruby

{"Tom" => 33, "Rebecca" => 17, "Siri" => 5}.select{ |_, age| value >= 18 }

Haskell

filter (\(_,age) -> age>= 18) [("Tom", 33), ("Rebecca", 17), ("Siri", 5)]

Clojure

(filter (fn [[_ age]] (>= age 18)) {"Tom" 33 "Rebecca" 17 "Siri" 5})

Scala

List(("Tom" , 33), ("Rebecca", 17), ("Siri", 5)).filter{ case (_, age) => age >= 18}

Rust

[("Tom", 33), ("Rebecca", 17), ("Siri", 5)].into_iter().filter(|&&(_, age)| age >= 18);

JavaScript

Object.entries({Tom: 33, Rebecca: 17, Siri: 5}).filter(([_, age]) => value >= 18)

Erlang

[T || {_, Age} = T <- [{"Tom", 33}, {"Rebecca", 17}, {"Siri", 5}], Age > 18].

CLisp

(remove-if-not #'(lambda (x) (>= (cadr x) 18)) '(("Tom" 30) ("Rebecca" 17) ("Siri" 5)))
@jpsim
Copy link

jpsim commented May 25, 2017

It's also convention in Ruby to use the _ identifier to indicate that a variable is unused:

{"Tom" => 33, "Rebecca" => 17, "Siri" => 5}.select { |_, value| value >= 18 }

@tmspzz
Copy link
Author

tmspzz commented May 25, 2017

@jpsim gotcha

@AliSoftware
Copy link

I don't think what allows that kind of syntax on other languages are the "tuple unsplattng" feature (that got removed by SE-110).
I think that what allows such syntaxes in other languages is that select/filter implementations of those languages for Dictionary are defined to take a closure with 2 arguments (instead of one tuple argument).

If you declare such a function in Swift, you can have a similar syntax in Swift too even after SE-110:

extension Dictionary {
  filter(_ isIncluded: (Self.Key, Self.Value) throws -> Bool) rethrows -> Self
}

Which could totally be added to the Swift stdlib the same way Dictionary was improved thanks to SE-100.

@tmspzz
Copy link
Author

tmspzz commented May 25, 2017

@AliSoftware point taken but this wont scale for functions with more than 2 parameters

@AliSoftware
Copy link

AliSoftware commented May 25, 2017

@blender Can you give me an example of such a function, for example in Ruby?
I mean, in Ruby select is explicitly defined on Hash as select {|key, value| block} → a_hash so it definitely expect a block with 2 arguments, not a block with a single argument being magically unsplat…

Also, the terms you are using (functions with more than 2 parameters) are revealing. Functions taking functions with 2 arguments are not affected by SE-110. Only functions taking functions with one argument of type tuple (2-tuple or 3-tuple or N-tuple) are affected), so I really think you're blinded by that distinction and that if we had that extension on Dictionary in stdlib already you'd never have realised that change by SE-110, because 99% of the use cases it affects today (and I totally agree the way it affects them today is bad, I'm not disputing that, but again to improve that let's go forward, not backwards) are filter/forEach/map/flatMap calls on… Dictionary, which are the main functions in stdlib that accept functions taking a single tuple argument rather than 2 distinct arguments.

BTW I think I personally never had to use that feature (that you seem to miss with SE110) with tuples of more than 2 elements. The only thing that SE-110 are gonna affect I think are functions that took a single argument of type tuple with 2 items, and 99% of those functions are the ones on Dictionary, for which we could (and should) have a better signature anyway (one that takes 2 parameters instead of a single tuple). So besides those cases (which I agree SE-110 now makes look bad at call site, I don't deny that at all) I don't think there are much more cases — even in other languages tbh — where you're gonna need them. And those cases (functions on Dictionary should be addressed by improving their signatures anyway.

@tmspzz
Copy link
Author

tmspzz commented May 25, 2017

@AliSoftware I can't (I just know basic Ruby) and maybe it's not possible. However filter is just an example. The point I wanted to make extends beyond that. Even if just one other language can do it (Haskell) I don't think that sacrificing expressivity for tooling speed is a valid compromise. Especially when this is not the case in other languages.

@AliSoftware
Copy link

AliSoftware commented May 25, 2017

And as stated on Slack, I much prefer consistency over magic. So instead of going backwards and revoke SE-110, I'd much prefer going forward and make a new proposal to have a way for deconstructing tuples directly. For example I'd love to see a SE proposal to allow this kind of syntax, rather than reverting SE-110 (and bringing back ambiguous function calls if we revert it):

arrayOfTuples.filter { _: (a,b) in print(a, " - ", b) }

Which in fact is almost possible today, using:

arrayOfTuples.filter { tuple: (a,b) in print(tuple.a, " - ", tuple.b) }

So such a proposal would simple ad the ability to use anonymous arguments for deconstructing tuples.

This way, with that feature (and in addition of the extension on Dictionary I suggested above) we'd have (1) improved type checker performance (because we removed the ambiguity thanks to SE-110), (2) still nice syntax and call sites (thanks to proper variants of filter/forEach/map/flatMap/… functions on Dictionary) and (3) still a way to deconstruct a tuple (but explicitly to keep consistency and avoid type-checker nightmare) easily if needs be (which again I don't believe will be used that much once we improved Dictionary's filter/forEach/map/flatMap signatures, but at least if we do need that deconstruction for the 1% of remaining cases, we can)

@tmspzz
Copy link
Author

tmspzz commented May 25, 2017

@AliSoftware good suggestion. I will change the wording of the title and the content to reflect your point.

@AliSoftware
Copy link

i don't think making tooling easier at the expense of the people who use tools is the right tradeoff. Especially when this is not the case in other languages.

@blender: Again:

  1. Where you think other languages offer that magic-unsplatting possibility, I don't think it's the case for all and I think you misinterpret the availability of dedicated function overloads on Hash/Dictionary for these languages as magic unsplatting while it's just a proper definition of those methods on Hash which takes 2 arguments (and not one single tuple arg)
  2. The tooling and the state before SE-110 was already at the expense of the users of the tooling — because it was one of the reasons why the type checker was slow. If you imagine that we have SE-110 which allows a speed increase on the type checker, but also add the extensions on Dictionary to take closure with 2 params (instead of a single 2-items tuple param), and the suggestion I added to deconstruct a tuple explicitly in my last comment, with all those 3 features we could have all the features that you need, + consistency + a Swift compiler and type checker that works faster. Sure that would give you less excuses to go take your coffee or switch to youtube to watch some cats videos while you're compiling, but I think that consistency, speed and still having the expressiveness with the solutions I suggest is a best way forward than trying to revert things and go backwards into a world of inconsistency and magic

@tmspzz
Copy link
Author

tmspzz commented May 25, 2017

@AliSoftware I think I will have other occasions for coffee and cat videos so I'm all up for speed while keeping expressivity :)

@AliSoftware
Copy link

AliSoftware commented May 25, 2017

Define this:

prefix operator *

prefix func *<A1,A2,R>(f: @escaping  (A1, A2) -> R) -> ((A1, A2)) -> R {
  return { (tuple: (A1, A2)) -> R in
    f(tuple.0, tuple.1)
  }
}

prefix func *<A1,A2,A3,R>(f: @escaping  (A1, A2, A3) -> R) -> ((A1, A2, A3)) -> R {
  return { (tuple: (A1, A2, A3)) -> R in
    f(tuple.0, tuple.1, tuple.2)
  }
}

// We could add some for tuples with 4 items, but I don't think people have a lot of real-world cases with tuples with more than 4 items (at that point, create a struct already)

Then even after SE-110 you can:

func foo(_ x: Int, _ y: Int) -> Int {
  return x + y
}

[(1,2),(3,4),(5,6)].map(*foo) // valid even after SE-110

let o1 = Observable<Int>.from(1,2,3)
let o2 = Observable<Int>.from(4,5,6)
Observables.zip(o1, o2, combine: *foo)

Note that, even before SE-110, in Swift 3 you were already not able to do this anymore:

let tuple = (1,2)
foo(tuple)
// error: passing 2 arguments to a callee as a single tuple value has been removed in Swift 3
// foo(tuple)
// ^  ~~~~~~~

So I'm not sure why people are so upset with SE-110 being applied to closures while the exact same rule was already applied to functions but nobody rioted back then ^^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment