docs/types/intersection-types.md
Intersection types play an important role in Enso type hierarchy and its visual representation. Having a value that can play multiple roles at once is essential for smooth live programming manipulation of such a value.
Intersections types are created with the use of
& operator. In an attempt to represent
Complex numbers (with real and imaginary component) one may decide to create a
type that is both Complex and Float when the imaginary part is 0:
type Complex
Num re:Float im:Float
plain_or_both self =
if self.im != 0 then self else
both = self.re : Complex&Float
both # value with both types: Complex and Float
Having a value with such intersection type allows the IDE to offer operations available on all individual types.
Creating a value of intersection types is as simple as performing a type check:
self : Complex&Float
However such a type check is closely associated with conversions. If the value doesn't represent all the desired types yet, then the system looks for conversion methods being available in the scope like:
Complex.from (that:Float) = Complex.Num that 0
and uses them to create all the values the intersection type represents.
<!-- Just as demonstrated at https://github.com/enso-org/enso/commit/3d8a0e1b90b20cfdfe5da8d2d3950f644a4b45b8#diff-c6ef852899778b52ce6a11ebf9564d102c273021b212a4848b7678e120776287R23 -->[!NOTE] Note that if a
Float.from (that:Complex)conversion were available in the scope, anyComplexinstance would be convertible toFloatregardless of how it was constructed. To ensure that such mix-ins are only available on values that opt-in to being an intersection type (like in theComplexexample above where we include theFloatmix-in only ifself.im == 0), we need to ensure that the conversion used to create the intersection type is not available in the default conversion resolution scope. Thus it cannot be defined in the same module asComplexorFloattypes, but instead it should be defined in a separate module that is only imported in the place that will be constructing the multi-values.
When an intersection type value is being downcast to some of the types it already represents, these types become its visible types. Any additional types it represents become hidden (unless open check is requested).
The following operations consider only the visible part of the type:
However the value still keeps internal knowledge of all the types it represents.
Thus, after casting a value cf:Complex&Float to just Complex, e.g.
c = cf:Complex:
c will only consider methods defined on Complexc can only be passed as argument to methods expecting Complex
typeFloat parameter is expectedAs such a static analysis knows the type a value
has been cast to (the visible part) and can deduce the set of operations
that can be performed with it. Moreover, any method calls will also only accept
the value if it satisfies the type it has been cast to. Any additional
remaining hidden types can only be brought back through an explicit cast. To
perform an explicit cast that can uncover the 'hidden' part of a type write
f = c:Float or inspect the types in a case expression, e.g.
case c of
f : Float -> f.sqrt
_ -> "Not a float"
Remember to use f.sqrt and not c.sqrt. f in the case branch has been cast
to Float while c in the case branch only can be cast to.
[!WARNING] Keep in mind that while both argument type check in method definitions and a 'type asserting' expression look similarly, they have slightly different behaviour.
f a:Float = a g a = a:FloatThese two functions, while very similar, will have different behaviour when passed a value like the value
cabove. The functionfwill fail with a type error, because the visible type ofcis justComplex(assuming the conversion toFloatis not available in the current scope). However, the functiongwill accept the same value and return it as aFloatvalue, based on the 'hidden' part of its type.
[!NOTE] In the object oriented terminology we can think of a type
Complex&Floatas being a subclass ofComplexand subclass ofFloattypes. As such a value of typeComplex&Floatmay be used whereverComplexorFloattypes are used. Let there, for example, be a function:rubyuse_complex c:Complex callback:(Any -> Any) = callback cthat accepts
Complexvalue and passes it back to a provided callback function. It is possible to pass a value ofComplex&Floattype to such a function. Only operations available on typeComplexcan be performed on value in variablec.However the
callbackfunction may still explicitly cast the value toFloat. E.g. the following is valid:rubyboth = v : Complex&Float use_complex both (v-> v:Float . sqrt)This behavior is often described as being open to subclasses. E.g. the
c:Complexcheck allows values with intersection types that includeComplexto pass thru with all their runtime information available, but one has to perform an explicit cast to extract the other types associated with such a value.
This behavior allows creation of values with types like Table&Column to
represent a table with a single column - something the users of visual live
programming environment of Enso find very useful.
Table.join self right:Table -> Table = ...
Such a Table&Column value can be returned from the above Table.join function
and while having only Table behavior by default, still being able to be
explicitly cast by the visual environment to Column.
There is a special form of intersection type check that can be used to perform a type check, but avoid downcasting a value (as the narrowing check does). Use:
cf : Complex&Any
to verify the cf value is a Complex number, but allow any other visible
types it has to remain visible.
Every value in Enso is of Any type. Static
type checking treats Any type in
a special way - anything can be done with it. Any method invocation is
(potentially) valid. Thus combining Complex&Any check does two things:
ComplexAny type)Such a combination is useful for static development tools (like editor code
completion) that can offer methods of Complex type for such a value in hand.
Dynamic development tools benefit as well as all the visible types after such
a type check still remain visible and can directly be used.
When an intersection type is being checked against a type it doesn't
represent, any of its component types can be used for
conversion. Should there be a Float to Text
conversion:
Text.from (that:Float) = Float.to_text
then Complex&Float value cf can be typed as cf:Text. The value can also be
converted to another intersection type like ct = cf:Complex&Text. In such
case it looses its Float type and ct:Float would fail.
In short: when a conversion is needed to satisfy a type check a new value is created to satisfy just the types requested in the check.
There are two slightly different places where type checking occurs:
inc value:Integer by:Integer=1 -> Integer = value+bynew_value = value:IntegerThese type checks are almost the same, except small difference with respect to intersection types. While the function signature checks consider only visible part of a type, the explicit type checks "try hard" and also include the hidden part of a type. As a result there two checks maybe yield a different result:
id x -> Text = x
t1 = v:Text
t2 = id v
The values t1 and t2 are guaranteed to be of type Text if the type check
succeeds (and doesn't Panic). However it is possible that t1 becomes Text
and t2 definition panics. Imagine a value defined as
both = 42 : Integer&Text # requires a conversion to Text to be around
v = both:Integer # hides the Text type
This behavior is important for static type checking which operates only on visible types of a value. Otherwise it wouldn't be possible to report meaningful type mismatch errors.
The small difference in treating hidden types implies that there is a difference in following two definitions...
id x:Text -> Text = x
id x = x:Text
...with respect to intersection types. Both type check the returned value to
Text, but the first one only considers visible types, while the second one
considers also hidden types.
Another implication of treating hidden types differently implies that open type check behaves differently in
id x:Text&Any -> Text&Any = x
id x = x:Text&Any
again. The first one only considers visible types, while the second one considers also hidden types. As a project of this behavior, there is also a difference in a simple Any type check:
id x:Any -> Any = x
id x = x:Any
the first one is a no-op as every value in Enso is of type Any. The second
explicit cast of x:Any however considers the hidden types and makes them
all visible. Explicit cast to Any instructs the runtime to unhide all types
of a value and make them visible.
A value of an intersection type is equal with other value, if all values it has
been cast to are equal to the other value. E.g. a value of Complex&Float is
equal to some other value only if its Complex part and Float part are equal
to the other value. The hidden types of a value (e.g. those that it can be
cast to, if any) aren't considered in the equality check.
The order of types isn't important for equality. E.g. Complex&Float value can
be equal to Float&Complex if the individual components (values it has been
cast to) match. As implied by (custom)
equality rules the hash of a value
of intersection type must thus be a sum of hash values of all the values it
has been cast to. As a special case any value wrapped into an intersection
type, but cast down to the original type is == and has the same hash as
the original value. E.g. 4.2 : Complex&Float : Float is == and has the same
hash as 4.2 (in spite it can be cast to Complex).