Help with where syntax in Julia v0.6

You seem to not like at all the idea of doing what Jeff has said a few times makes sense and is doable, making a thunk and evaluating it only when the type parameters have all been bound.
Besides being what people expect, it also is more efficient than the workaround, which involves figuring out these parameters every time an instance of the type is constructed, instead of figuring things out when the concrete type is actually instantiated (i.e. when all of the parameters are bound).

Part of what makes Julia performant is doing as much as possible earlier, at compile-time, instead of leaving things to be done at run-time.

This is shear and utter nonsense. You can only run functions during runtime. Inference constant folds expressions very frequently, which is what yields the better performance, not the type parameterization. I agree with Jeff that this is doable. I just think this assumption that the compiler somehow cares about whether https://github.com/vtjnash/ComputedFieldTypes.jl is a macro or builtin syntax is misinformed. And I think that changing the syntax lowering to give the ComputedFieldTypes.jl behavior instead of an error would just increase that confusion (the lowering changes are essentially the same in either case, although emitting an error is much easier to test).

I have a parameterized type MyFloat{NBits}.
I create from that a concrete type MyFloat{256}.
Then I need to create 100 million of instances of these types.

At the time I create the type MyFloat{256} (i.e. the parameter is bound), I want to have it calculate that I need
4 64-bit unsigned limbs on a 64-bit platform, or 8 32-bit limbs on a 32-bit platform.
With the change that we’ve been talking about, those calculations would occur once, when the type was instantiated, instead of the current workaround of having to have the type actually have extra parameters, one for the number of limbs and another for the type of the limb, and to have the constructor perform the calculations (and that would be happening 100 million times, instead of just once).

Your ComputedFieldTypes has the same performance issue, AFAICT.

That’s not how the change works. Type instantiation cannot run code since types are created during compilation, not runtime. Any runtime computations in the definition must happen each time that type is created when the program is run.

That’s not how compilation works. Our compiler may be simplistic right now, but it’s not that badly broken.

1 Like

@jameson Have you bothered to benchmark this, or look at the generated code?
I have, and it bears out what I’ve been trying to get across to you.
Before making inflammatory statements (i.e. “sheer and utter nonsense”), you should check out the facts first.

Also, you seem to have a misunderstanding as to what is going on when types are created.

Code definitively is run when a type is created, and you can use functions that return the types used for the fields.
The issue is that those functions are run when the type is created, which doesn’t work if the calculations contain any type parameters, since those are not yet bound.
That is where Jeff agreed (when asked both at JuliaCon 2016 and JuliaCon 2017, @andyferris is a witness!) that that could be fixed (as I had already pointed out) by created a thunk if there were any type parameters used in the expression that would get evaluated when a concrete type was instantiated from that parameterized type, i.e. when the parameters were all bound)

The workaround of doing the calculations when an object is created, instead of fixing the bug of prematurely evaluating the type variables, is simply too slow for many of the cases where this would be useful (such as for new faster numeric types, where you want to create a struct (immutable) with a number of limbs that depends on the number of bits of precision desired and the machine integer size).

I’m talking about when parameterized types are created, when concrete types are actually instantiated from those parameterized types, when values are constructed, and when potentially costly calculations are done, i.e. either at the time that the concretes types are instantiated, or when every value is constructed (as happens now with the workaround).

As far as I understand (from inspecting the code, and inserting debugging statements into the code called when types are created), what currently happens is:

  1. when a parameterized type is created, any expressions to calculate the types of the fields are executed. If a function is called with a type parameter, it is passed something of type TypeVar, which indicates that it is bound or unbound.
    This is used for example with the Maybe(T) function, which returns a Union{T, Void}, which I’ve seen in various places in Julia code.
    This type is cached by Julia.
  2. when a concrete type is created from a parameterized type, any fields whose type was a type parameter, or a type parameterized by the type parameters, have those type vars replaced by their bound values.
    This concrete type is then also cached by Julia.

What needs to happen is:

  1. when a parameterized type is created, thunks are created for any expression that uses a type parameter in the body of the type definition. Otherwise, expressions without type variables can be immediately evaluated, as occurs now.
  2. when a concrete type is created from a parameterized type, any thunks are evaluated with the type vars replaced by their bound values.

This would eliminate the need for the “hidden” parameters needed by the current workaround, which can make the types harder to understand, and expose internal implementation details.

Yes, I have. Did you remember to mark the functions you called as @pure?

It doesn’t eliminate them – the values still have to be stored somewhere – it just hides them better (so that reflection can only retrieve them from fieldtypes, instead of via reflection on the parameters).

I want to emphasize here that this is the type of comment that kills a community. At JuliaCon we had a talk discussing good manners in open source, and I invite everyone to look it up when it is out.

Dozens of smart people cannot be wasting their time shitting each other, instead we should all be polite and have empathy when we discuss on this forum.

7 Likes

@ScottPJones I just tried to copy/paste your solution, but it is giving me an error in Julia v0.6:

ERROR: MethodError: no method matching ntyp(::TypeVar)
Closest candidates are:
ntyp(::Type{A<:AP{N,T}}) where {N, T, A<:AP{N,T}} at REPL[4]:1

As I said, that’s how you could do it, IF you could perform calculations directly on parameters.
There is a work-around for now (which you can do directly, or use @jameson’s package that automates it),
that adds extra parameters for the values that need to be calculated, and then uses a constructor that performs the calculations.

The difference is that instead of performing the calculations once, when the concrete type is instantiated with all the parameters bound, they happen every time an instance of that type is created, which is one of the reasons I strongly believe this needs to be fixed.

1 Like

Thank you @ScottPJones, I thought your code snippet was working already in Julia v0.6, I had sketched similar answer in the past on this forum: Recovering parameter type from parameter list

I really miss this feature in Julia.

This is overly pessimistic. The sorts of computations required here are (by definition) entirely determined by the given types. That means that they are typically compiled to directly down to constants, no runtime computation needed at all.

2 Likes

Correct. Also, memoizing something in a type has never guaranteed that we won’t recompute it – we only usually ensure that is true for the typeof of an instance, and then only so far as we can make the changes hard to observe. This is somewhat analogous to generated functions (we don’t guaranteed they won’t be recomputed each time it is run, we simply guarantee that it won’t recompile while its being run). Although comparing type fields to functions also fails on several key points, so I wouldn’t bother with trying to push that comparison very far.

I’m probably missing something very obvious but why does this:

struct Foo{T<:Number}
  name::String
  state::T
  
  function Foo{T}(name::String, state::T) where {T<:Number}
      new(name, state)
  end
end

Foo("a", 1.)

give a MethodError: no method matching Bar(::String, ::Float64) ?

I’m expecting the above to be identical to

struct Bar{T <: Number}
  name::String
  state::T
end

Bar("a", 1.)

which works fine.

Hi @ValdarT, I think it has to do with the fact that whenever you define an inner constructor for a type, Julia doesn’t define outer constructors automatically, you have to write them yourself. Someone else can correct me.

The inner constructor you’ve written requires you to specify T explicitly. An inner constructor for which T is inferred from the state argument should be written as

struct Foo{T<:Number}
  name::String
  state::T
  
  function Foo(name::String, state::T) where {T<:Number}
      new{T}(name, state)
  end
end

This is called an outer-only constructor (although I’m not a big fan of that name). Edit: or maybe it isn’t called an outer-only constructor? I’m confused.

2 Likes

Very interesting @tkoolen, first time reading that section of the documentation, thanks for sharing.

we have methods & parametric methods, and for consistency’s sake, maybe it’s a good idea to classify constructors by constructors & parametric constructors instead of inner & outer?

The way I understand it, the “outer-only constructor” term refers to the inner constructors being blocked by the constructor provided, and hence only the outer constructors being available.

That solves it. Thank you! And it really is a bit confusing.

I think that if you had never learned the old syntax the new one would be far less confusing – in the new way, Foo{T} only ever means Foo with T as a type parameter, and you define methods for that type the same way you call it. I explained more in this post:

6 Likes