-
Yorick Peterse authored
A few months ago I turned Boolean from a trait into an object. Back then the trait setup for Boolean was rather horrible and I thought using a regular object would be better. Unfortunately the use of an object brought various issues with it. For example, every method defined on True or False would also have to be defined (as a dummy method) on Boolean itself. It was also impossible to support code such as this: def example(other: do -> Boolean) -> Boolean { if true: { True }, false: { other.call } } This wouldn't work because "Boolean" is not guaranteed to implement the same methods as "True". One solution to this problem would be to introduce union types and define "Boolean" as a union like so: type Boolean = True | False While tempting I feel that union types are the wrong approach. Every union type can be replaced by a trait and traits in general are much more pleasant to work with. For example, consider the following union type: type Number = Integer | Float Now let's say this type is defined somewhere in the standard library or some other piece of code we can't easily modify, and we use it in a whole bunch of places. If we at some point want to also support a Complex or Rational we'd have to either define our own type, or somehow ask the author of the "Number" type to extend it. Both cases are a pain. With traits on the other hand this would not be an issue as we can simply implement it where necessary and we're good to go. Because of the above issues we now define "Boolean" as a trait. To support this one can now define a trait and later redefine it, but _only_ if the trait is empty. This allows us to define "Boolean" in the "std::bootstrap" module and later refine it in "std::boolean". Without this we would not be able to bootstrap the runtime as various modules depend on "Boolean" being present from the very beginning. With these changes we also no longer need the GetBooleanPrototype instruction and thus it has been removed. We also removed && and || in favour of "and" and "or". Both these methods take a block that is only evaluated when necessary. This means that instead of this: foo && bar You would write: foo.and { bar } This has the added benefit of automatically grouping expressions, making it easier to chain message sends. For example, instead of this: (foo && bar).if_true { ... } You can now write this: foo.and { bar }.if_true { ... } To make all of this work I also had to make some changes to the type system. In particular we need to support downcasting of blocks in certain cases. Take the following piece of code for example: def or(other: do -> Boolean) -> Boolean { if true: { True }, false: { other.call } } Here the block passed to `true:` would be inferred as `do -> True` while the block passed to `false:` would be inferred as `do -> Boolean`. This would then produce a type erro because `do -> Boolean` is not compatible with `do -> True`. To support this we now check if in such cases we can downcast the expected type (`do -> True` in this example) to the given type. Currently we only downcast the return type of a block, but we may add support for other cases in the future.