3 ms·
I'm not a Crystal developer but I think this is accurate: The way Crystal's type inference works, if you have a variable that sometimes can be assigned a Foo a
by ezrast 8y ago
I'm not a Crystal developer but I think this is accurate:
The way Crystal's type inference works, if you have a variable that sometimes can be assigned a Foo and sometimes can be assigned a Bar, then that variable will have the inferred ("generated") union type (Foo | Bar). This usually works great, but the compiler runs into performance issues if these unions become very large (more accurately, it happens when the number of distinct such unions that gets passed to a particular method is large, for which a large possible union space is a precondition).
In practice, this tends to happen in class hierarchies with many descendants from a single root class. As a somewhat kludgey workaround, the compiler will simply forget about the union in these cases and use the parent class instead. For example, a method that looks like it should take a (Bat | Cat | Dolphin | Dog | KoalaBear | Hog) will actually just take a Mammal. This can be bad if Mammal doesn't implement all the common capabilities of those specific subtypes. For example, adding an Echidna subclass that doesn't implement the birth_live_young method can cause calls to `cat_or_dog.birth_live_young` to fail even in places that type inference "should" be able to determine that `cat_or_dog` will never hold an Echidna. However, it's deemed a necessary evil unless/until the compiler can be reworked to handle such cases without exploding compile times.
- zem 8y agoone possibility is to generate a "protocol" (a class implementing the intersection of the methods of all the types in the union) and fall back to that instead.
- weberc2 8y agoThat makes some sense, but I don't understand why the explosion would be combinatorial and not linear.
- ezrast 8y agoIf a method can handle n different types at runtime, then there are 2n - 1 possible union types that can be passed to the method: A, B, C becomes A, B, C, (A | B), (A | C), (B | C), (A | B | C), and that's without taking into account generics and nested type hierarchies. Not all of these unions will necessarily come up organically in code, but the class hierarchy of AST nodes used within the compiler was large enough that it was causing issues in real life.
- weberc2 8y agoCorrect me if I'm wrong, but you don't need to generate every permutation to determine whether or not a given union type can be passed into a method, right? If the parameter is of type `A | B` and the function takes `A | B | C`, you don't need to generate the whole space to determine that `A | B` satisfies `A | B | C`. Also, for v1.0 of the language, it doesn't seem like it would be a big deal to put the onus on developers to recast their `A | B` to a `A | B | C`.
- ezrast 8y agoIt's not just about validating types. If B and C are implemented sufficiently differently, the compiler output for a function that takes (A|B) will be different than for the same function that takes (A|C), and both of those outputs can end up coexisting in the final executable. Unfortunately I don't know enough about the compiler internals to go into further detail. As for manual recasting, sure, it's possible, but it's not a great tradeoff. Either you do type inference correctly and have developers crashing the compiler unless they recast their variables in seemingly arbitrary ways, or you cheat on inference and have developers getting "undefined method" errors on classes they didn't think they were using until they learn the rules. Ultimately it was decided that the latter was less common and easier to deal with in practice (just add a PlacentalMammal subclass to your class hierarchy that includes Cat and Dog but not Echidna). It's not a decision anyone is thrilled with but I think it's reasonable.
- weberc2 8y agoI guess "larger binaries at v1 but correct" and "slightly less ergonomic at v1 but correct" both seem like pretty good tradeoffs to my mind, but at this point we seem to be pretty deep into speculation.