It's kind of tricky talking about "the compiler", since there's a lot that
goes on to get from user-facing Julia code to native machine code. That
being said, here's how I'd explain the issue to myself, under the very
rough and ready approximation that "the compiler" is the engine that
produces low-level code that the computer can understand (more or less)
directly from the high-level Julia code that we write. Hopefully somebody
else will chime in to help correct any errors/fill in the gaps.
Let's say you write a script with some functions and load that into a Julia
session. Now you call one of those functions on a particular object, call
it `x`, of type `T`. At this point, Julia makes what you could call a
preliminary pass through that function's code seeing what happens to the
*type* of an object of type `T` as that object is passed through the
function and through any other functions subsequently called on `x`. This
is Julia's type inference system at work, gathering information for the
compiler to use when it produces the machine code that will handle the
actual *value* of `x`. The more information that is available to the
compiler at this point in time, the better, since the compiler can use that
information to justify performing any number of fancy optimizations that
result in faster machine code. After the compiler has gathered all the
information it can, it passes that information off to its various parts
which produce the machine code that handles the actual value of `x`.
So, why is `MyType` preferable to `MyStillAmbiguousType`? Well, take a look
at the types of `m` and `t`:
julia> type MyStillAmbiguousType
a::FloatingPoint
end
julia> type MyType{T<:FloatingPoint}
a::T
end
julia> t = MyStillAmbiguousType(3.2)
MyStillAmbiguousType(3.2)
julia> m = MyType(3.2)
MyType{Float64}(3.2)
julia> typeof(t)
MyStillAmbiguousType
julia> typeof(m)
MyType{Float64}
When the compiler gets to the type inference stage, it can figure out a lot
more about what a value like `m` will do when passed around than it can for
a value like `t`, even though at this point in time the compiler doesn't
know the value of either `m` or `t`. This is because `m`'s type carries
that `Float64` parameter. Now, there are a couple of difficulties with your
proposed solution, i.e. branching on the type of `t.a`. The first is that
introducing unnecessary branching can hurt performance in other ways. The
second -- and more pertinent -- is that, though the compiler may be able to
reason about `f1` and `f2` at compile time (though this will be limited for
reasons discussed below), it still can't reason about `f` at compile time.
All the compiler can say about `f` is that it might return a Float32 or
that it might return a Float64, and nothing more. Now all downstream
interactions with whatever gets returned by `f`will have to take that
ambiguity into account -- the returned object will have to be "boxed" and
its type checked to make sure that the proper instructions are followed.
However, if the compiler knew, for instance, that `f` could *only* return
an object of type Float64, then any downstream interactions with that
object could be streamlined based on that knowledge.
As far as your second question goes, note that there's nothing any more
restrictive about declaring `x::Float64` on its own or wrapped inside of a
type. The point here is that you could, within a function, set `t.a =
Float32(3.2)`, and t will still be of type `MyStillAmbiguousType`. So, the
compiler may still be limited in its ability to reason about what can go
happen to t in `f1` or `f2` even after you branch on the initial value of
`t.a`. However, you can't change `m.a` to a Float32 -- any attempt to do so
will just result in that Float32 value being reinterpreted back into a
Float64.
On Monday, July 20, 2015 at 6:21:17 PM UTC-4, Mathew wrote:
>
> I'm not clear on the following element in Julia FAQ:
> http://julia.readthedocs.org/en/latest/manual/faq/#how-do-abstract-or-ambiguous-fields-in-types-interact-with-the-compiler
>
> In a nutshell, the FAQ defines two types:
>
>
> > type MyStillAmbiguousType
> > a::FloatingPoint
> > end
> > t = MyStillAmbiguousType(3.2)
> >
> > type MyType{T<:FloatingPoint}
> > a::T
> > end
> > m = MyType(3.2)
> >
> > The fact that the type of m.a is known from m‘s type—coupled with the
> > fact that its type cannot change mid-function—allows the compiler to
> > generate highly-optimized code for objects like m but not for objects
> > like t.
>
> I don't really understand this passage (I don't know anything about
> compilers to begin with!)
>
> - If the acceleration comes from the fact the type is known before the
> function is runned, can't the compiler create itself functions with all
> fields of the type as argument, rather than the type as an argument, ie
> transforming
>
> f(t::MyStillAMbiguousType)
>
> into
>
> f(t::MyStillAMbiguousType) = f(fa::FloatingPoint = t.a)
>
> More bluntly, another solution would be something like
>
> function f(t::MyStillAmbiguousType)
> if typeof(f.a) == Float64
> f1(t)
> elseif typeof(f.a) == Float32
> f2(t)
> end
> end
>
>
> - If the acceleration comes from the fact that "its type cannot change
> mid-function", why don't we replace every arguments of a function by an
> type that encloses its fields, to declare that the type won't change in the
> body of the function. For instance, instead of `f(x::Float64)`, define
>
> type EnclosedFloat
> x::Float64
> end
> f(x::EnclosedFloat)
>
>
>
>