One of their developers mentioned a few months ago that type aliases were still an upcoming feature[1], though I do agree with your statement in general.
Yes, Julia also has type aliases [1]. This is a pretty obvious feature when you have parametric types. The article feels a bit like the author has pretty limited experience with other languages besides Java prior to trying Ceylon.
Ceylon has a potentional to become the best statically typed language available. I really love the elegance combined with the pragmatic focus on readability, toolability and type-safety. Ceylon is so refreshing compared to Scala. Unfortunately it didn't get a lot of attention on HN, I'm quite curious what other people's impressions are.
Ceylon is very similar to Kotlin, but I much prefer Kotlin, mostly because it so seamlessly interoperates with Java (more so than any other JVM language I'm aware of). Also, Kotlin already has kickass IDE support.
Kotlin also compiles to JS and has typesafe Groovy-style builders. I don't like the union and intersection types, and think Kotlin being "a syntax skin over the Java platform" is its main advantage.
Thing is, neither Kotlin nor Ceylon (nor Scala, IMO) add something fundamentally necessary over what's available in Java (8). They mostly provide productivity enhancements. For me, added productivity is really nice, but it doesn't justify switching a language, unless it's a 10x productivity boost (Clojure is different; it provides a whole new way of thinking and managing state and data, which is very important for concurrency - this is big). So in that case, I'd rather give Java a facelift than change to something else altogether. Kotlin is just a Java facelift. It's trivial and transparent to mix Kotlin and Java classes, even in the same package.
Productivity and how fun is it to use a language are about the only metrics that I care about. Even small productivity increase can be a reason to switch language.
I don't understand the Clojure bit - do you mean STM?
Some of the smaller decisions the Ceylon team has made also seem nicer than the Kotlin equivalent. For example I prefer the Ceylon "if(exists x) {}" to the Kotlin "if(x != null)" - even though the Ceylon is further from the Java syntax.
_Please_ try Ceylon IDE, and let us know where it's nonasskicking. We've worked very, very hard on it, and we think we have something really useful and usable, but of course we very much welcome feedback.
FTR, I personally pushed almost 240 klocs to this project over the past two years.
For some of us, it comes down to whether we prefer Intellij or Eclipse. Having used both for a number of years, I prefer the former over the later (though for reasons that would be outside the scope of the Kotlin or Ceylon Plugins themselves).
Ah, OK, fair enough, well, Bastien and Matija have recently been making very good progress on the IntelliJ plugin for Ceylon, and since we expect to be able to reuse quite a bit of underlying code from the Eclipse plugin, we hope to something useful quite soon. Stay tuned :)
Kotlin is a very nice replacement for Java but I really like the ambition and thought being shown by the Ceylon development team. I sure do hope that they get an IntelliJ plugin completed sometime in the future.
I have very little experience with Haskell, I didn't really like it, it didn't seem to have strong focus on peoductivity, my view is that:
* Imperative programming is more straightforward in Ceylon.
* The same holds for functions with side-effects.
* Type safety is similar, Ceylon currently doesn't have type classes and generalized algebraic data types but will be probably added according to the FAQ.
* Ceylon is less type-inferred, which makes it more readable IMHO (1. regular syntax 2. type annotations are not just for the compiler, but for documentation too). Also - less confusing compiler errors.
* In general, Ceylon feels pragmatic, Haskell academic.
> * The same holds for functions with side-effects.
Just about every haskell program I write has functions with side-effects.
> * Ceylon is less type-inferred, which makes it more readable IMHO ... 2. type annotations are not just for the compiler, but for documentation too).
So having to write type annotations all the time is an advantage for you? You can just as easily add type annotations to Haskell, are you saying that since it is compiler enforced with Ceylon, that Ceylon code as a whole is easier to read?
Furthermore, doesn't less type-inferred just mean "type inference isn't as good", or was that a design decision?
> Also - less confusing compiler errors.
Any examples of confusing Haskell compiler errors? I personally haven't seen any memorably confusing compiler errors from Haskell. Have you seen any with Haskell or is that just something you've heard about?
Any examples of compiler errors where Ceylon beats Haskell?
Any examples of compiler errors where Ceylon beats Java?
> 1. regular syntax
By "regular syntax" do you mean "algol derived or inspired"?
> * In general, Ceylon feels pragmatic, Haskell academic.
This is interesting, because it's something I've heard from just about everyone I've asked about Haskell. Recently I have delved into Haskell myself and haven't found it true.
For instance, arrows are something many consider "academic", but they enable the very pragmatic and real world advantage of handling HTML like:
-- haskell
-- added bonus of type safety through type inference!
images tree = tree >>> css "img" >>> getAttrValue "src"
-- python
def images(tree):
imgs = [elem for elem in tree if elem.tag == "img"]
sources = [img.get('src') for img in imgs]
return sources
-- alternate python
def images(tree):
return [ img.get('src' for img in filter(lambda i: i.tag == "img", tree)]
If my understanding is correct the Haskell above is (at least could) take advantage of stream fusion which has the real world advantage of being really efficient and really fast.
> * In general, Ceylon feels pragmatic, Haskell academic.
This is something that is entirely subjective. I will have to look into Ceylon more in the future. I will say that from the outside Haskell looked very academic for me, but after writing some code, Haskell feels/seems to be/has proven to be a very pragmatic language that doesn't deserve a reputation of being "academic".
As a whole I wish that many other languages weren't just dismissed as "academic" so that we could innovate more quickly and improve technology as a whole. There are valid arguments to be had about much of the experimental stuff, but calling something "academic" is usually a cop out in my experience.
> So having to write type annotations all the time is an advantage for you?
Well, there are very interesting tradeoffs here. If you want to go the "whole hog", i.e. HM type inference, where you don't need to annotate the types of parameters, you can't have subtyping (at least not in the usual sense yes yes I know about OCaml which requires an explicit upcast).
Now, I personally think subtyping is great, and it's even greater when you introduce first-class union+intersection types into the mix. So there's definitely a place for languages without full HM type inference.
Next, you could still have a system with more type inference than what Ceylon supports, but you would need an arbitrary number of passes over the code to infer types, and you would introduce the possibility of typing errors which can't be pinned down to a single erroneous expression.
So Ceylon supports a more limited sort of type inference: local type inference. The Ceylon compiler can infer and check all expressions in a single pass over the code, and every typing error is localized to a single expression.
And y'know what: that turns out to be a pretty reasonable thing practically speaking, since documenting the types of exported APIs is frankly a good thing to do anyway, and helps the people who come along later and look at your code.
> Just about every haskell program I write has functions with side-effects.
That's because just about every program needs side-effects. My point was that side-effects are more straightforward in Ceylon, you don't need monads.
> So having to write type annotations all the time is an advantage for you? You can just as easily add type annotations to Haskell, are you saying that since it is compiler enforced with Ceylon, that Ceylon code as a whole is easier to read?
Yes. I think that it makes the language more regular and less messy. When you look at foreign code, you can always immediately see the types. But I have little experience with Haskell or Scala, so I may be wrong.
> Furthermore, doesn't less type-inferred just mean "type inference isn't as good", or was that a design decision?
My guess is that it was a design decision. I think they wanted to have a interface model that's understandable and predictable (you can easily say whether type annottions are necessary or not).
> Any examples of confusing Haskell compiler errors? I personally haven't seen any memorably confusing compiler errors from Haskell. Have you seen any with Haskell or is that just something you've heard about?
No, I admit that this was something I read on Ceylon's site (I think) and it's quite possible I'm wrong here.
> By "regular syntax" do you mean "algol derived or inspired"?
Not sure how to put my intuitive understanding of "regular" into words... I would say that it means that the code has relatively few visual patterns, there's lower visual variance. It's like with tidy (= regular) vs messy (= irregular) rooms. It's easier to "understand" tidy room.
> This is interesting, because it's something I've heard from just about everyone I've asked about Haskell. Recently I have delved into Haskell myself and haven't found it true.
For me it's evident that Ceylon has very pragmatic focus on real-world productivity. I was excited about the design decesions and trade-offs they've made, I've often thought "yes, exactly my thoughts!" when reading the language design FAQ. On the other hand, I simply don't see the focus on productivity in Haskell. Do monads really increase productivity? Would a team of Haskell programmers implement a stackoverflow.com copy faster than, let's say, a team of similarly intelligent and experienced C# programmers?
> For instance, arrows are something many consider "academic", but they enable the very pragmatic and real world advantage of handling HTML like:
The Python code could be:
def images(tree):
return [img.get('src') for elem in tree if elem.tag == "img"]
It's almost as succint but seems more regular to me, it's just the old good comprehension, nothing fancy.
>> Any examples of confusing Haskell compiler errors? I personally haven't seen any memorably confusing compiler errors from Haskell. Have you seen any with Haskell or is that just something you've heard about?
> No, I admit that this was something I read on Ceylon's site (I think) and it's quite possible I'm wrong here.
FTR, I think you might have got mixed up here. Definitely good error reporting is a very major priority for the language, and I think we generally do better than Java at least when generics are involved, but we've never made any comparisons positive or negative to Haskell.
Don't know anything about it. But it's good that there's a revival of interest in languages for scientific computing. When I was in uni all we had was Fortran :-/
I consider it a general-purpose language, it's mostly the libs and community that give it scientific bias. I've used it as a Python replacement for scientific computing recently and if the libs were more mature, I would choose it over Python for, say, web dev. Can highly recommend, especially for language designers :)
Sequence literals, intersection types, type aliases, declaration-site variance, functions and methods? Got them.
Modules? Meh. The OSGi thing has always struck me as hideously overengineered; I've never understood what problem it's supposed to solve.
Nullable types? The trouble with doing this at the language level is that you can't then treat them as plain old objects. Can I write a method that's polymorphicly nullable? Can I write a method that polymorphicly handles "nullable" or Future or Validation, like scalaz's "sequence"?
Union types? Yeah, proper union types would be good.
> Modules? Meh. The OSGi thing has always struck me as hideously overengineered
And that's _exactly_ why you need modularity built into the ecosystem. OSGi and Maven _are_ hideously overengineered, and they're overengineered because there's no common model built into the language, toolset, and platform.
> I've never understood what problem it's supposed to solve.
Management of versioned dependencies between libraries and components developed by different teams. This is absolutely central to what software engineering is all about.
> Nullable types? The trouble with doing this at the language level is that you can't then treat them as plain old objects.
I don't think you've understood what this is. In Ceylon, null is the singleton instance of the perfectly ordinary class Null. A nullable type is just a union type of form Null|X, for any type X.
> Can I write a method that's polymorphicly nullable?
Sure. You should see the crazily cool stuff we can do with stuff like the signature of max(). You'll be impressed, I promise!
After that, check out signatures of operations like Set.union(), Set.intersection(), coalesce(), compose(), flatten(), curry(), etc, to see a bunch of other things you just can't write in Scala.
> functions and methods? Got them.
Does scala have toplevel functions? I thought they always had to be nested inside some object?
But my real big problem with Scala here is it just doesn't have proper function and tuple types that you can abstract over. Instead you have F, F1, F2, F3 ... F22 and Tuple2, Tuple3, ... Tuple22. That to me is just rubbish.
> > Can I write a method that's polymorphicly nullable?
> Sure. You should see the crazily cool stuff we can do with stuff like the signature of max(). You'll be impressed, I promise!
From the docs:
> A nonempty iterable is an iterable object which always produces at least one value. A nonempty iterabe type is written {String+}. Distingushing nonempty streams of values lets us correctly express the type of functions like max():
> While this is interesting, is it enforced by the compiler?
Of course. That's the whole point. I really think you've missed the nature of Ceylon. Don't imagine, that just because it has a syntax that looks friendly and harmless and familiar, that it doesn't have a really killer type system under the hood.
> i.e. is the following a runtime, or compile time error?
I assume this is the implementation [1](took me a while to find the type to see how the compiler might interpret it).
I read the definition as (ignoring the implementation):
return a Value or Absent when given an Iterable<Value,Absent> where Value is Comparable and Absent is Null
Correct me if I am wrong,
but there doesn't appear to be anything linking the nullability of the return value based on the possible emptiness of the input value. How does the compiler check this constraint (which sounds a lot like a dependent type relation)?
Upon further reading of the Iterable type[2], I have more questions, does "Nothing" satisfy "Null"?
| If not, then it looks to me like passing in a non-empty iterable (Iterable<X,Nothing>) is a type error.
| If so then I would imagine that it becomes harder to enforce the constraint,
likely because the return type can still be Null regardless of input restrictions.
Does this use extra type inference on the expressions inside the function to do the enforcement,
for instance using the "exists" operator to perform type set subtraction between {Value|Null} and {Null} on the true branch.
This implies that if I re-write the max function to have a single exit point (`ret = Null; if (exists values.first) { ...; ret = ...; } return ret;`) then I don't get the same guarantees?
Also the fact that Null is referenced by the "exists" operator also implies that it is part of the language specification, and not actually definable in any meaningful way as a user type.
Digging a bit deeper, I was very disappointed to read this snippet [3]:
Nothing is considered to belong to the module ceylon.language. However, it cannot be defined within the language.
If I read correctly, that means that I cannot implement the same behaviour/enforcement with completely user defined types.
> Correct me if I am wrong, but there doesn't appear to be anything linking the nullability of the return value based on the possible emptiness of the input value.
Sure there is. The second type parameter of Iterable encodes that, see the definition of Iterable.
> does "Nothing" satisfy "Null"?
Of course. Nothing is the bottom type, it satisfies every type.
> If so then I would imagine that it becomes harder to enforce the constraint, likely because the return type can still be Null regardless of input restrictions.
I don't know what you mean by this. An Iterable<X,Nothing> cannot return Null from its first attribute. Check the declaration of Iterable.first.
> Does this use extra type inference on the expressions inside the function to do the enforcement, for instance using the "exists" operator to perform type set subtraction between {Value|Null} and {Null} on the true branch.
Approximately correct. But "exists" is not type subtraction, it's intersection with the class Object. Cool, huh?
> This implies that if I re-write the max function to have a single exit point (`ret = Null; if (exists values.first) { ...; ret = ...; } return ret;`) then I don't get the same guarantees?
I don't follow. You can't rewrite the function to do anything unsound because the compiler won't let you. Our type system is sound.
> Also the fact that Null is referenced by the "exists" operator also implies that it is part of the language specification, and not actually definable in any meaningful way as a user type.
That's not correct. Go and check the definition of Null. It's just a regular class, as advertised. There is no special behavior defined in the language spec. Sure, the exists operator is defined by the language spec to mean "is Object", but that's just trivial syntax sugar.
I guess the problem here is that you're used to languages full of weird special cases and ad hoc behavior. Ceylon is not like that. Ceylon's type system is very simple and these fancy constructs are layered over it as pure syntax sugar. It's a very different approach to other languages. But in the end it's much more satisfying. You'll just need a little mental adjustment to get used to it.
> If I read correctly, that means that I cannot implement the same behaviour/enforcement with completely user defined types.
Wha?! Nothing is the bottom type! The empty set. You want to define your own bottom types? That's not possible (nor desirable) in any language I know of. There's just no reason why you would want to write a class with no instances.
> I don't know what you mean by this. An Iterable<X,Nothing> cannot return Null from its first attribute. Check the declaration of Iterable.first.
This made me sad again[1], but then this made me happier[2] (internalFirst implied it is defined in the compiler, finding all of the pieces that make up the definition was a bit of a pain since it is dispersed across so many files), but I think I didn't fully understand union types (and that Nothing was bottom) when I wrote the grand parents logical conclusions.
:t Iterable<X,Nothing>.first ==>
X|Nothing ==>
X (because union with empty is an identity)
therefore inside max, the variable first will have type X when given an Iterable<X,Nothing>.first. I also see how it cannot be re-written as I proposed because there is no guarantee that Absent will be of type Null.
> That's not correct. Go and check the definition of Null. It's just a regular class, as advertised. There is no special behavior defined in the language spec. Sure, the exists operator is defined by the language spec to mean "is Object", but that's just trivial syntax sugar.
I did check the definition of Null, I was merely pointing out that although it is a user definable type, the language specification does mention its existence here[3] and here[4] which is where I looked to try to find out what the "exists" keyword meant. Specifically:
in the case of an exists condition, a type whose intersection with Null is not exactly Nothing and whose intersection with Object is not exactly Nothing, or
Later on it says "exists x is equivalent to is Object x, and..." but then why reference Null earlier?
> I guess the problem here is that you're used to languages full of weird special cases and ad hoc behavior.
While I thank you for your time answering my mis-understandings of union types, please do not shoe horn me.
My main programming activities are using OCaml and Haskell, both of which have very few ad-hoc language features, if any.
> I was merely pointing out that although it is a user definable type, the language specification does mention its existence
Sure, the language spec _mentions_ Null, in order to define the syntax sugar X? and exists x. But that doesn't mean that Null has any special place _in the type system_.
> My main programming activities are using OCaml and Haskell, both of which have very few ad-hoc language features, if any.
OK, great, sorry for making assumptions. In my defense, I just didn't quite understand why you were having such a hard time believing that Ceylon could do things like this without adhoc special cases.
> Management of versioned dependencies between libraries and components developed by different teams. This is absolutely central to what software engineering is all about.
Let me put it this way: I develop in Java (and Scala) using maven, without ever touching OSGi. What is the problem I'm supposed to be having that OSGi would be solving? Because I've never seen it.
> I don't think you've understood what this is. In Ceylon, null is the singleton instance of the perfectly ordinary class Null. A nullable type is just a union type of form Null|X, for any type X.
You're right, the short syntax put me off (it's something I worry about in scala too).
> Sure. You should see the crazily cool stuff we can do with stuff like the signature of max(). You'll be impressed, I promise!
> After that, check out signatures of operations like Set.union(), Set.intersection(), coalesce(), compose(), flatten(), curry(), etc, to see a bunch of other things you just can't write in Scala.
I tried to do that, but it looks like you don't have an equivalent to javadoc/scaladoc with the standard library docs publicly available? Either that or google can't find them.
(And I'd still like to hear whether you can do something as general as scalaz's sequence. It's a function that takes F[G[A]] to G[F[A]] for any F and G with suitable typeclasses (Traverse and Applicative, IIRC), so the same function works for Set[Future] or List[Validation] or Vector[MyMonad]) - I don't think you can do that without higher kinded types. And it appears as an extension method on the F[G[A]]).
> Does scala have toplevel functions? I thought they always had to be nested inside some object?
Point, but I find it hard to care. You can stick them in a package object if you really need to emulate one at toplevel.
> But my real big problem with Scala here is it just doesn't have proper function and tuple types that you can abstract over. Instead you have F, F1, F2, F3 ... F22 and Tuple2, Tuple3, ... Tuple22. That to me is just rubbish.
It's a bit rubbish yeah, I agree. Shapeless 2 makes it painless to treat tuples as HLists (using implicit macros - a concept which admittedly would have utterly horrified me six months ago) which means you can work around it in practice.
> What is the problem I'm supposed to be having that OSGi would be solving? Because I've never seen it.
The problem OSGi solves is having two versions of the same module as part of the same assembly. This is quite possible if you're using third-party libraries that in turn depend on another third-party library.
But I think you're missing something here. Ceylon's module system supplants both OSGi _and_ Maven. It's not just an alternative to OSGi, it's a whole architecture for modularity.
> I tried to do that, but it looks like you don't have an equivalent to javadoc/scaladoc with the standard library docs publicly available?
> I don't think you can do that without higher kinded types.
Correct, we don't have type constructor polymorphism in the official language, and even if we did, we wouldn't use it for this particular problem. Our equivalent APIs are based on (lazy, non-copying) stream-processing.
Now, I do have a working implementation of type constructor polymorphism in a branch of the Ceylon typechecker, but I probably won't merge it because:
1. we don't have a really convincing use for it, and
2. I see it as essentially useless without type constructor argument inference, and Ross and I doubt that a robust and decidable inference algorithm exists for a language with subtyping.
> Point, but I find it hard to care.
Speaking for myself, I don't like unnecessary distracting complexity, even trivial unnecessary distracting complexity.
> Shapeless 2 makes it painless to treat tuples as HLists (using implicit macros - a concept which admittedly would have utterly horrified me six months ago) which means you can work around it in practice
Brrrrr. And then you Scala folks tell the rest of us that Scala is not complex and that we're full of FUD or worse ;-)
FTR, the way Ceylon models this is the way this is the same way it is modeled in mathematics. That is to say, in mathematics, a function is a set of ordered pairs, where the first element of each ordered pair is a tuple. The notion of a tuple is itself defined by recursion. Therefore, in Ceylon, a function type is Callable<AType, ATupleType>, and Tuple is is a class with a recursive definition. Clean, intuitive, abstractable.
Very cool type annotations. As a mostly Java developer, I wish API's had this level of documentation, to have it built into the language and type system is wonderful. As a curiosity, is there any tooling to infer or generate the type annotations for methods? I imagine that would make it easier to learn for those who have never worked with a powerful type system before (as you say, even Java's can be confusing).
> FTR, the way Ceylon models this is the way this is the same way it is modeled in mathematics. That is to say, in mathematics, a function is a set of ordered pairs, where the first element of each ordered pair is a tuple. The notion of a tuple is itself defined by recursion. Therefore, in Ceylon, a function type is Callable<AType, ATupleType>, and Tuple is is a class with a recursive definition. Clean, intuitive, abstractable.
Sounds like HList. AIUI performance concerns were why early versions of Scala used the current style of tuple (and alas compatibility requires us to keep using it). Do you avoid instantiating a JVM object for each entry? Are you sacrificing Java compatibility, or is the tradeoff somewhere else?
Having now paid attention to who you are, how do you handle ORM in what I presume is a more immutability-oriented language? Right now I've got a nice reactive spray webapp backing on to a pool of database-access threads (because JDBC still does blocking I/O) which are deliberately as dumb as possible, and it feels like I'm fighting hibernate all the way; hibernate seems to expect sessions to belong to a single thread, objects to belong to a single session, and updates to be performed by mutating an object rather than copying it. It works for now, but it feels like every time I try to do something new (like having an entity with a collection in) something breaks; I'm not saying it's impossible to use hibernate in this kind of environment (I mean, the reason I'm using it at all is that it's still better than the alternatives) but it feels like I'm cutting against the grain and everything is harder than it should be. Perhaps I should be using monads to accumulate operations to be performed in a session, and handing those off to my database threads? (but even that won't work if I want to update an object based on the result of an async call).
Anyway, to get back to the point, does Ceylon have a good story for this problem? Is it going to include a next-gen hibernate replacement? Is there some nice way to do session-in-view and carry a session for a particular web request around even as processing that request happens on several threads? Are you still expecting people to represent database rows as mutable objects? Am I thinking about this all wrong?
> Do you avoid instantiating a JVM object for each entry?
Yes, a tuple, at the VM level is just a wrapped array. Our compiler backend does magic with certain language module types.
> Are you sacrificing Java compatibility, or is the tradeoff somewhere else?
Well, I dunno, a tuple looks like a regular Ceylon list to a Java client, but I don't think that really breaks _compatibility_ as such. Depends how you define "compatible".
> Having now paid attention to who you are, how do you handle ORM in what I presume is a more immutability-oriented language?
No clue. That's something we'll have to think about in 2014.
> hibernate seems to expect sessions to belong to a single thread, objects to belong to a single session, and updates to be performed by mutating an object rather than copying it.
Yes. That's definitely what Hibernate expects. It was never designed for copy-on-write. Because at the end of the day the database contains a bunch of mutable state and Hibernate reflects that in memory. I can't imagine that the solution to this problem is going to be straightforward.
> Anyway, to get back to the point, does Ceylon have a good story for this problem?
Not yet.
> Is it going to include a next-gen hibernate replacement?
I hope so, but I can't promise because I have not spent time thinking about it.
> Well, I dunno, a tuple looks like a regular Ceylon list to a Java client, but I don't think that really breaks _compatibility_ as such. Depends how you define "compatible".
Fair enough. The idea that the object graph I see in scala corresponds 1:1 to the object graph I see in java for the same object is comforting, but I haven't actually tried working without it. (OTOH I have concrete cases where I need existential types for compatibility, much as I wish I didn't)
> The idea that the object graph I see in scala corresponds 1:1 to the object graph I see in java for the same object is comforting
Surely. Not merely comforting, but useful.
But it entails deep compromises in the design of the type system that we're just not willing to make, especially since Ceylon is primarily a cross-platform language, not just a JVM language.
> OTOH I have concrete cases where I need existential types for compatibility, much as I wish I didn't
Right. Whether we'll eventually have to throw in the towel on this one and add use-site variance to Ceylon, purely to get better interop with Java, is a very interesting and bothersome topic.
> Modules? Meh. The OSGi thing has always struck me as hideously overengineered
One of the next versions of Java will ship with a module system. This will likely be good enough and not worth the hassle of having to deal with two competing module systems.
> Sure. You should see the crazily cool stuff we can do with stuff like the signature of max(). You'll be impressed, I promise!
I have looked it, and maybe I'm not seeing it, but it looks like Ceylon is repeating all the things which have been wrong with Comparable in the first place.
Given that Ceylon had a clean sheet to start with, I'm surprised that no better solution has been found.
The signature is intimidating and basically combines the non-extensibility of Comparable with the declaration-site boilerplate of Comparable—with none of its benefits.
- If your type was written by someone who didn't care to implement Comparable, bad luck!
- If the type has a Comparable implementation which differs from what you need, bad luck, too!
- You need the type to be comparable in multiple ways? Again, bad luck!
If any of this use cases came up, it would mean adding a new method which takes an Comparator to all APIs working with Comparable. Leading to more boilerplate and all the pain usually associated with defining Comparators.
Scala has things to complain about, but I think it's hard to claim that they haven't nailed this case down perfectly.
Consider:
case class Person(firstName: String,
lastName: String,
age: Int) extends Ordered[Person] {
def compare(that: Person): Int =
this.lastName compare that.lastName
}
val persons =
Person("Ann", "Miller", 32) :: Person("Bob", "Smith", 17) ::
Person("Charlie", "Miller", 71) :: Nil
val personsSortedByFirstName = persons.sorted
// Person doesn't implement Ordered (Comparable)
// or we want to use a different Ordering?
val orderByAge = Ordering.by[Person, Int](_.age)
val personsSortedByAge = persons.sorted(orderByAge)
None of the drawbacks, all of the benefits!
Additionally, the signature of the sorted method would be much simpler and more readable in Scala, too:
def sorted[T : Ordering] = ...
Compare that to:
shared Element[] sort<Element>({Element*} elements)
given Element satisfies Comparable<Element> => ...
But I guess messing with Comparable as an upper bound is the best thing one can do without typeclasses. :-/
> Instead you have F, F1, F2, F3 ... F22 and Tuple2, Tuple3, ... Tuple22. That to me is just rubbish.
Looking at what you did with Tuples (basically a linked list), I'm not sure that there is a large difference.
Scala's approach seems to be more in line of YAGNI (if you need that 523-Tuple, you should think a bit about your data model) and performance (no pointer chasing, all values are just a method call away).
Anyway, if one wanted tuples-as-linked-lists, one could just use Shapeless' HList library, which has a few benefits over Ceylon's tuples, too, as far as I see:
val hlist = 1 :: "str" :: 42.31 :: 'sym :: HNil
val str: String = hlist(1) // Not possible in Ceylon, afaik
> One of the next versions of Java will ship with a module system.
I certainly hope so, since we always planned to replace JBoss Modules with Jigsaw when that becomes possible. But we still don't know if the Jigsaw module system will be made available for all consumers or just for the JDK itself.
> If your type was written by someone who didn't care to implement Comparable, bad luck! - If the type has a Comparable implementation which differs from what you need, bad luck, too! - You need the type to be comparable in multiple ways? Again, bad luck!
Um, you've totally missed the point. Go and have another look.
And sure, since you bring it up, we can certainly add a version of max() that accepts a comparator function. It would be a method of Iterable, probably.
And sorry, but I can't imagine why I would want or need typeclasses to solve that problem. Sure, I could use typeclasses, if I were a solution in search of a problem ;)
> I'm not sure that there is a large difference.
You can't abstract over tuple and function -arity in Scala. So yes, there's a big difference. Look at the definition of compose() in Ceylon.
> and performance (no pointer chasing, all values are just a method call away).
At the VM level, tuples are represented as arrays. So there is no real performance penalty.
> Anyway, if one wanted tuples-as-linked-lists, one could just use Shapeless' HList library, which has a few benefits over Ceylon's tuples, too, as far as I see:
In Ceylon, without any macro library, you can write:
value tuple = [1, "str", 42.31];
String str = tuple[1]; //totally possible in Ceylon
Seriously dude, _just try it out_, you're desperately searching for reasons to not like this thing! But it really has cool stuff to offer you. Give it a fair chance!
Sure, _of course_ you're going to find stuff in Scala that you can't do in Ceylon. Scala is one of the most huge and complex and feature-rich languages _ever_! But you'll also find plenty of stuff you can do in Ceylon that you can't do in Scala, if you care to look.
> And sorry, but I can't imagine why I would want or need typeclasses to solve that problem. Sure, I could use typeclasses, if I were a solution in search of a problem ;)
Oh, now I'm a bit confused ... are you really saying that you think that the version with the unreadable and cryptic signature, which is non-extensible in any way and repeats all of Java's mistakes is a good solution (or the best solution available in Ceylon)?
> You can't abstract over tuple and function -arity in Scala. So yes, there's a big difference. Look at the definition of compose() in Ceylon.
I just looked at the documentation of it: compose(g,f)(args)==g(f(args))
Wouldn't this just be ...
val list = List(1,2,3)
val f: (List[Int] => Int) = list => list(0)
val g: (Int => Int) = i => -i
(g compose f)(list)
... in Scala? What am I missing?
> At the VM level, tuples are represented as arrays. So there is no real performance penalty.
Nice, so this would be more like Shapeless' HArray then!
> In Ceylon, without any macro library, you can write:
> value tuple = [1, "str", 42.31];
> String str = tuple[1]; //totally possible in Ceylon
Great, that's nice. I just checked it and you are completely correct of course! Is there also stuff for filtering elements by type, mapping, grouping? I couldn't find much in the code.
> Oh, now I'm a bit confused ... are you really saying that you think that the version with the unreadable and cryptic signature, which is non-extensible in any way and repeats all of Java's mistakes is a good solution (or the best solution available in Ceylon)?
I'm saying that it's cool as hell that the following is all well-typed:
And that this behavior makes the slightly cryptic signature of max() worthwhile.
Sure, sure, this is not the most general possible signature for max(), since it is only for types with a natural order. We could and should add some form of max() for comparator functions, which we already have for sorting.
P.S. By the way, there are examples of functions where type classes are nice, for example sum(), where the type class lets you correctly express the sum of zero elements. (But max() is not one of these functions.) So in fact, we would definitely like to experiment with type classes in a future version of Ceylon, especially since the idea probably meshes very nicely with our reified generics.
> What am I missing?
You're missing that the definition of compose() is abstracted over the arity of the second argument function in Ceylon. It's _one_ function, not a different method defined for each Fi type. Which means that I can write similarly-abstracted functions that use compose().
> Is there also stuff for filtering elements by type, mapping, grouping?
Sure. Tuple is a subtype of List, which is a subtype of Iterable, which defines all stream-processing functions. FTR, an [Integer,String,Float] is a List<Integer|String|Float>.
> I'm saying that it's cool as hell that the following is all well-typed:
Ah ok, that's certainly nice. There is a library in Scala though which let's you switch APIs from T to Option[T] to Try[T] to Future[T] to ... with a single import. But I think in a language were nulls are the primary means to encode a missing value, this possibility is nice and makes a lot of sense! (Although I really think it should be investigated how the signatures can be simplified. If you think it's so great (given the constraints Ceylon has, imho yes, it is) it probably makes sense to support this by default and not by making the library author do extra work.)
> You're missing that the definition of compose() is abstracted over the arity of the second argument function in Ceylon.
But aren't you pretty much focusing on implementation details here? If I said “Tuples in Ceylon ... what a mess! Look at this type signature:
” I think you should rightfully call me out on that this is nothing which matters to a user and a developer will pretty much ever see
[Integer, String, Integer]
I think it's kind of the same way for the current Tuple encoding in Scala. One could switch to a better representation and most users probably wouldn't even notice.
And just for the record: I think you have made very convincing points here that Tuples can be handled and I think moving Scala into the direction of HLists/HArrays makes a lot of sense!
Anyway, I have to say that this discussion has been downright pleasant and some of my reservations towards Ceylon, which were fueled by seeing you in some conversations/talks (both in the Hibernate era and in early Ceylon) which left the impression of “wow, what a jerk” (sorry :-/) to me have been resolved with this discussion.
Again, this discussion was very nice, resulted in learning a few new and interesting things and I'm taking nothing but good impressions with me!
I think if both communities took more care to explain the different reasoning, design decisions and philosophies behind the languages when giving talks or on conferences in the future (I saw a few early Ceylon presentations in which I felt Scala's design decisions were unnecessarily misrepresented, but of course the same applies to any Scala presentation if the subject would touch Ceylon, too), this could be a huge win-win situation for both communities. I think the real competitor of Ceylon/Scala is not Scala/Ceylon, but the sad status quo of our industry. If the communities combined their voices to get the message out that there are modern languages and better development approaches existing today, I think this would benefit everyone.
While I'm probably too spoilt by Scala to enjoy programming in Ceylon, I think that especially compared to its current direct competitor in mindshare, Ceylon has a lot of good ideas and tries very hard to give its users the best experience possible, given the constraints you have given yourself when designing the language. With Ceylon, tons of people will learn about the usefulness of union/intersection types in a real-world setting. This benefits our profession as a whole and would certainly not be the case if the language was just some prettified Java (ok, ok, let's not get me started on Ceylon's syntax :-D).
> But aren't you pretty much focusing on implementation details here?
No, not at all, I'm talking about the ability to abstract over things. Can I write a single higher-order function that operates over functions with any -arity? In most languages, no. In Ceylon, yes.
> Again, this discussion was very nice, resulted in learning a few new and interesting things and I'm taking nothing but good impressions with me!
In general, the quality of discussion here is better than in other places.
> I think if both communities took more care to explain the different reasoning, design decisions and philosophies behind the languages when giving talks or on conferences in the future
Sure, but when you have nasty and idiotic twitter stuff poisoning the air, it's hard to expect people to adopt a fair-minded and technical approach to the topic. People should just get off twitter. It's a community-wrecker. There's almost nothing you can say on twitter that rises above the level of insulting.
You beat me to this comment. Ceylon brings nothing new to the table that would make me jump the Scala ship.
What's the point of having Nullables if we're still forced to do glorified if(*.isNull) ... else ... checks? I'd much rather use Scala's Option type that has Map and GetOrElse already baked in.
Also, the tooling around Scala is starting to mature. The Kotlins and Ceylons of the world are way behind Scala in this regard as well.
> What's the point of having Nullables if we're still forced to do glorified if(*.isNull) ... else ... checks? I'd much rather use Scala's Option type that has Map and GetOrElse already baked in.
Sorry, but Option is just not better. One advantage of using a union type is that if I have a List<String?>, I just have a List<String|Null> (which maps to a plain List<String> at the JVM level) not a whole list of wrapper objects in a List<Option<String>>.
And of course Ceylon has sufficient syntax sugar so that you can write things like:
print(nameOrNull else "Gavin);
You definitely don't have to do an "if (exists...)" every time you encounter a null value.
> Also, the tooling around Scala is starting to mature. The Kotlins and Ceylons of the world are way behind Scala in this regard as well.
WDYM?! I strongly beg to differ. Have you seriously tried out Ceylon's IDE? Or are you just speculating?
And it's not like one is stuck with Option either.
I think the ability to choose which abstraction is the best for a specific use case (Option, Either, Validation, Future, etc.) is the major benefit over languages which just bake the handling into the language.
Sure, those languages could just ship with additional classes which did that, but the problem is that they wouldn't be able to use the syntax sugar which is built-in for the special case of nulls as well as other language constructs like Scala's for-comprehensions which work for all types of a certain shape, not a single, built-in special-cased item.
Again, nullable types are not "baked into" the language. They are syntax sugar for a union type.
Also, Ceylon doesn't need Either, since that is just a workaround for not having union types.
Seriously, union and intersection types are _really_ fundamental notions in set/type theory, and having them as basic constructs of the language makes the language _much_ more expressive.
I seriously urge you to try it out. You'll love them, I guarantee it.
The improvements over Java are things addressed in (typed) clojure.
I get the feeling that comparing Ceylon to Java is like comparing a 1990s car against a 1980s car.
That's not to say the comparison is invalid but if (and only if) the goal is to have/make/evaluate a good car you should compare it to 2010s cars.
I develop mostly in Java with quite a bit of Clojure. While my favorite IDE is NetBeans, when I work on a mixed Java/Clojure project I use IntelliJ IDEA with the Cursive[1] plugin. You can freely jump from a Clojure call to Java code, and when you rename a Java method, it refactors the Clojure code as well.
> I really enjoyed programming in Clojure, especially 4clojure.com, an amazingly fun way to learn a new language.
4clojure.com is fun for learning basic syntax and problem solving, but it doesn't really teach you how build applications with Clojure.
> * Worse readability (parens, less regular). That's the #1 problem of Clojure.
I have to disagree with you here. Clojure's syntax is a lot more regular than Scala's or even Java's.
> * IDEs, including LightTable, are not even close to the usefulness of Eclipse or IDEA.
You can use Eclipse or IDEA, though the support for Clojure in these IDEs is not as good as it is for Java. This is only natural because Java is a lot older and statically typed.
> * Imperative code is easier to write and more importantly more readable in Java.
Naturally imperative code is easier to write and more readable in Java, because lambdas are not yet available. If you mean Java's imperative code is more readable than Clojure's functional then I would like to know if you have an equal amount of experience in these languages?
> * Formattig code for readability is PITA. In Eclipse, I usually just press Ctrl+Shift+F to autoformat after every edit.
I don't understand your point, you can autoformat Clojure code.
In my experiences much of what I considered "imperative code" could be written in a functional manner more easily in the functional host language, though the solution was usually very conceptually different.
Just wanted to say that I think union and intersection types are very interesting and powerful. Also agreed on the fact that Tuple22 and friends are not ideal. We are looking for ways to avoid that in a future version of Scala. I am certainly very interested in what you and the Ceylon community do in terms of language design and hope we can learn from each other.
Cheers, Martin, thanks for the note, I appreciate it! Well, there's already a couple of things we learned from you guys: FTR, Scala was the first place I ever saw declaration-site variance, which rocks. It's also the language that influenced me and the others to take type inference seriously. Definitely hope it's a two-way street :-)
Actually we're just talking about two different ways to represent the same concept. In the ML-derived languages, which traditionally did not have first-class union types - which I assume derives from the fact that they don't have _subtyping_, the workaround for representing A|B is to create an algebraic sum type Either which wraps the actual value we're interested in. For the case of null|B, there's a specialized Maybe or Option type.
Now, this pattern has recently been imported into object-oriented land by languages like Scala and now Java 8, because these languages also don't have first-class union types.
But in my opinion, that's the wrong path to have taken. In OO-land, we _do_ have subtyping, so things like unions and intersections are well-representable within the type system, and we don't need these workaround wrapper types.
There is no major difference between a nullable type and a maybe - the big difference is between non-nullable types and maybe. If you have a maybe I in my ignorance can assign a null to it, if you have a non-nullable type the compiler will prevent me from assigning a null to it.
As far as I can tell, you are right, although in my view maybes are one step ahead: they form a monad. (Not saying there's anything stopping you from doing the same thing with nullables, but still they're a slightly different concept.)