Skip to content

Instantly share code, notes, and snippets.

@acdenhartog
Last active March 14, 2016 09:41
Show Gist options
  • Save acdenhartog/a47fad6ebe8d32ff0ff5 to your computer and use it in GitHub Desktop.
Save acdenhartog/a47fad6ebe8d32ff0ff5 to your computer and use it in GitHub Desktop.
Why the update method should have been curried in the Scala language specification:
This text describes much of what is wrong with the update sugar in Scala.
Each part should be able to drop directly into a repl/worksheet and compile.
See 2-TL;DR:.scala for motivation.
/** Before explaining why, here is how the update method syntax ought to work:
*/
object OughtTo {
// Inner comments show acceptable syntax:
def update(a:T)(b:T){
// OughtTo(a) = b
}
def update(a:T,b:T)(c:T,d:T){
// OughtTo(a,b) = (c,d)
}
def update(as:T*)(bs:T*){
// OughtTo(as:_*) = bs:_*
// OughtTo(a1,a2) = (b1,b2,b3)
// OughtTo(a) = b
// OughtTo() = Nil:_*
}
def update()(b:T){
// OughtTo() = b
}
def update()(bs:T*){
// OughtTo() = bs:_*
// OughtTo() = Nil:_*
// OughtTo() = (b1,b2,b3)
}
def update(at:(Int,Int))(bt:(Int,Int,Int)){
// OughtTo(at) = bt
// OughtTo(a1,a2) = (b1,b2,b3)
}
type T = TypeNotPresentException //more like TypingNotPresentException amirite?
}
object Wat {
def update(a:Int,b:BigInt*){}
}
Wat(1) = List[BigInt](2,3,4):_*
Wat(1,2,3,4) = 5
Wat() = 5
//Wat(1,List(2,3,4):_*) = 5 //IntelliJ thinks this is okay too!
object SoWat{
type Tuple = (String,String,String)
def update(t:Tuple,n:(Int,String)){}
def update(t:Tuple){}
def apply(t:Tuple){}
}
SoWat() = ("this","()","magic")
SoWat("Come","back") = "here"
SoWat(("Number","of","parentheses")) = (2,"many")
SoWat("Apply","without","those")
/** demonstrates current scalac behaviour and language specification
*/
object AsOfNow {
def update( to:String){}
def update(a:(Int,Int,Int), to:String){}
def apply (a:(Int,Int,Int)){}
def update(a:Int , to:Any*){}
def update(a:Int , to:(Any,Any)){}
}
//AsOfNow = "error: reassignment to val"
AsOfNow() = "magical () makes it work"
AsOfNow((1,2,3)) = "need to use double parentheses"
AsOfNow(1,2,3) //but that is not needed for apply
AsOfNow(1,2) = "what method does this call?"
AsOfNow(1) = List(2,"how about this one?"):_*
AsOfNow(1) = (2,"works as expected but the method signature is ugly")
AsOfNow.update(1,'a',"one")
AsOfNow.update(1,"two",'b')
object Curried extends ExtraCurried {
def update( )(to:String){}
def update(a:(Int,Int,Int))(to:String){}
def update(a:Int )(to1:Any, to2:String){}
def update(a:Int )(to:Any*){}
def update(a:Int )(to:(Any,Any)){}
}
Curried.update()("that () is not so magical now")
Curried.update((1,2,3))("double parentheses")
Curried.update(1,2,3)("can now be removed")
//Curried.update(1,2)("that does not even exist...")
//The compiler correctly complains that these are all ambiguous:
//Curried.update(1)(List(2,"..."):_*)
//Curried.update(1)("...","...","...")
//Curried.update(1)("...","...")
trait ExtraCurried {
def update2(a:Int )(to1:String,to2:String){}
def update2(a:Int )(to:Long*){}
def update3(a:Int )(to1:String,to2:String){}
def update3(a:Char)(to:Long*){}
def updateVarArgs(a:String*)(to:String){
//Note: method renamed because varargs overload of any type will conflict with tuple coercion
}
}
//Curried.update2(1)("is still ambiguous","curried function demands that every part is unambiguous")
//Curried.update2(1)(10L,20L,30L,40L)
Curried.update3( 1 )("first part is no longer ambiguous","overloading the second half of update is no longer allowed")
Curried.update3('c')(10L,20L,30L,40L)
Curried.updateVarArgs("","","")("AsOfNow cannot normally use varargs on the lhs of an update.")
// Why the restrictions due to currying are a good thing:
object EqualityOverloading extends SomethingMysterious {
def update(a:Int, to:this.type){}
def update(a:Int, to:String){}
}
EqualityOverloading(1) = EqualityOverloading
EqualityOverloading(1) = "But what is the type of this thing that is being updated?"
EqualityOverloading(1) // Is this a String now? I set it to an EqualityOverloading before...
/** The way update is currently designed allows the = symbol to be overloaded with different types.
That makes no mathematical sense whatsoever!
Or what else is this syntactic sugar supposed to mean?
*/
//Instead one should really be using Either or something equivalent in such a situation:
object CurriedOverloading extends SomethingMysterious {
def update(a:Int)(to:Either[this.type ,String]){}
implicit val getEitherAny :this.type=>Either[this.type,String] = Left(_)
implicit val getEitherString: String=>Either[this.type,String] = Right(_)
}
CurriedOverloading.update(1)(Left(CurriedOverloading))
CurriedOverloading.update(1)(Right("now all the types make sense again"))
CurriedOverloading.apply(1) // It can only return one type of thing...
CurriedOverloading.update(1)("If necessary one can use an implicit conversion so that update seems overloaded again.")
// One can make it even more flexible...
object ImplicitSideEffects {
sealed trait Update
def update(a:Int)(to:Update){/*Update an constructor does everything now*/}
implicit val updateViaInt:Int=>Update=int=>new Update{/*any side effects*/}
}
ImplicitSideEffects.update(1)(20)
20:ImplicitSideEffects.Update // What could possibly go wrong?
// obviously, a private command pattern could be used to prevent this
trait SomethingMysterious {
def update(a:Int,to: => Nothing){
/*
Oh, look! update is overloaded in here too!
So it turns out, that calling apply(a) could maybe have returned an object, or a string,
it might have called a lambda that never returns at all.
That is to say, our expectation of what happens when we call apply(a)
depends largely on the types that we already know can be used with update(a) = _
By the principle of least surprise, it makes a massive amount of sense to avoid that type overloading,
or at least to make it explicit when the update(a) method is not straight forward,
as one must do if the updated method curried and overloaded!
*/
}
def apply(a:Int):Any = null
}
object CurriedInTheDotty {
/** In the future one might use type unions to overload the the = symbol.
* But here overloading = makes mathematical sense again.
* And there can be no hidden update methods.
*/
def update(a:Int)//(to: Int|String )
{}
/** blindingly obvious what type this should have.
*/
def apply(a:Int):Any = null
}
// There is a small technicality which must be addressed:
object OnAmbiguity {
def apply(a:Null)(b:Int)(c:Int,d:Int)={}
OnAmbiguity(null)(2)(3,4) // currying is allowed for the apply method.
// So why not for the update method?
// **Syntactically** the apply and update methods overload each other
// here we name them both x show what consequences this has:
def x/*apply*/(an:Int):Double = 0.0
def x/*update*/(an:Int)(to:Double){}
// Neither method can now be used due to ambiguity!
// So scalac needs some magical way to disambiguate.
def apply(a:Int) = OnAmbiguity
def update(c:Int,d:Double){}
OnAmbiguity(1) = 1.0
OnAmbiguity(1)(2) = 1.0
OnAmbiguity(1)(2)(3) = 1.0
OnAmbiguity(1)(2)(3)(4) = 1.0
// Here the way Scalac is deciding to call keep calling apply or update is by looking for that = symbol.
// If update were curried, this can lead to ambiguity:
object A {
def apply(b:Int) = B
def update(b:Int)(c:Int)(v:String){}
}
object B {
def update(c:Int)(v:String){}
}
A.apply(1).update(2)("")
A.update(1)(2)("")
// The original solution to this was: never allow update to be curried.
// If update were restricted only be curried _once_at_most_ then the ambiguity goes away.
// And it solves all the problems noted above.
// Also note that currently def update(...)(implicit ...) is allowed and behaves as one would expect.
}
/**
* What I recommend is the following:
* 1) deprecate the use of varargs and singe param tuple in the uncurried update method.
* 2) eventually deprecate and replace the uncurried update method entirely.
* 3) support the syntactic sugar for once curried update methods.
* and thus safely allow both varargs and tuple usage on either side.
*
*
*
* As for implementation:
*
* The main AST transformation is simpler than the current update method:
* OughtTo( $treeA ) = $treeB => OughtTo.update( $treeA )( $treeB )
*
* Afterwards all the regular syntactic sugar with tuple would need to be applied.
*
* However, there is still complication on the rhs:
* def update(...)(c:C,d:D){...}
* OughtTo() = (c,d)
* Requires an extra conversion of Tuple2(c,d) into the argument list {c,d}
*
* An easy way to bypass this is to add a method:
* def hidden_update(...)(to:(C,D)) = update(...)(to._1,to._2)
* And do something similar for Varargs:
* def hidden_update(...)(to:Product) = update(...)(bs.productIterator.asInstanceOf[Iterator[T]].toSeq:_*)
*
*
* One could also do much better than that by using macros.
* And certainly it could be implemented more neatly within the compiler itself.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment