Hacker News new | ask | show | jobs
by theseoafs 4672 days ago
I was very excited by this when I read the description, because a compiled language that looks like Ruby is exactly what I've wanted. Unfortunately I'm not super excited by the quirks of the implementation. For example:

    if some_condition
      a = 1
    else
      a = 1.5
    end
If I'm working in a compiled and typed language, the last thing I want is a language that automatically gives union types to variables. As far as I'm concerned, the type inference should fail at this point. In the above example, now I'm forcing the compiler to maintain a union which is going to have a pretty significant overhead on every computation the variable `a` is involved in.
2 comments

But isn't that how type inference works in most statically typed languages?

For example, in Scala: val x = if (some_condition) Employer else Employee

If Employer and Employee both derive from Person, then x will be of type Person. The run time uses dynamic dispatch to figure out how members are accessed from x.

If you want to constrain x, you need to specify the type explicitly.

Your example shows a different kind of behavior than the parent's example. A union type (t1 \/ t2) is a type that says "either this value has type t1, or it has type t2".

In your example, the type of x is not (Employer \/ Employee), it is their shared superclass - Person. The analogous example would be if

    val x = if (some_condition) Employer else Employee
succeeded even though Employer and Employee did not share a superclass. Very few languages use union types - Typed Racket comes to mind, and Algol apparently did too.
But actually in Crystal you will have the same behaviour:

  class Person
  end

  class Employer < Person
  end

  class Employee < Person
  end

  x = some_condition ? Employer.new : Employee.new
  # x is a Person+
This is not said in the "happy birthday" article (or anywhere else, IIRC).

In the beginning we typed x as Employer | Employee. But, as the hierarchy grew bigger compile times became huge. Then we decided to let x be the lowest superclass of all the types in the union (and mark it with a "+", meaning: it's this class, or any subclass). This made compile times much faster, and in most (if not all) cases this is what you want when you assign different types under the same hierarchy to a variable.

What this does mean, though, is that the following won't compile:

  # Yes, there are abstract classes in Crystal
  abstract class Animal
  end

  class Dog < Animal
    def talk
    end
  end

  class Cat < Animal
    def talk
    end
  end

  class Mouse < Animal
  end

  x = foo ? Dog.new : Cat.new
  x.talk # undefined method 'talk' for Mouse
That is, even though "x" is never assigned a Mouse, Crystal infers the type of "x" to be Animal+, so it really doesn't know which types are in and considers all cases.

Again, this is most of the time something good: if you introduce a new class in your hierarchy you probably want it to respond to some same methods as the other classes in the hierarchy.

Well, the happy birthday article has a section on "union types", and that code block has the comment "# Here a can be an Int32 or Float64". I just assumed that this meant a had the type Int32 | Float64. If the language doesn't actually have union types, then the article should probably be edited to reflect that (because it's very misleading on this issue).
It has union types. Right now if you do 1 || 1.5 it gives you Int32 | Float64. If you do Foo.new || Bar.new, and Foo and Bar are not related (except they both inherit from Reference), then you get Foo | Bar. If Foo and Bar are related by some superclass Super, then you get Super+.

If you do:

a = [1, 'a', 1.5, "hello"]

you get Array(Int32 | Char | Float64 | String)

In a way, the Super+ type is a union type of all the subtypes of Super, including itself, but just with a shorter name.

If your variable gets promoted to a union type, and all of the types respond to the methods you give them, then it will compile and run successfully (duck typing). Yes, it will have a small performance cost. If you then profile your app and find that the performance problem is that one, you go and fix it.

You can always have a static analyzer tool (that works, because Crystal is compiled) that can pin-point all the locations of union types. You can then put some type restrictions wherever you need them, to know where the union types come from.

The idea is that you can start prototyping something that works and is quite fast, and later you can always improve the performance without ever having to write C code.

Also, the union of an int and float will probably be just float so it won't have any performance overhead.