Can't write parametric constructor with no arguments?

224 views
Skip to first unread message

Patrick O'Leary

unread,
Apr 14, 2012, 1:32:23 PM4/14/12
to juli...@googlegroups.com
Consider:

julia> type Doh{T}
           a::T
           Doh{T}() = new{T}()
       end

julia> Doh{Int64}
Doh{Int64}

julia> Doh{Int64}()
no method Doh()
 in method_missing at base.jl:60

I'd like an empty constructor for the parametric doubly-linked list implementation I'm writing as a base for an LRU cache, but I can't appear to write one. What am I missing?

Stefan Karpinski

unread,
Apr 14, 2012, 1:39:02 PM4/14/12
to juli...@googlegroups.com
This is a bit confusing, but inside the type declaration you don't put the {T} on the method declarations. I'm not sure what this does, but the inner {T} might shadow the outer {T}. What you want is this:

julia> type Doh{T}
         a::T
         Doh() = new()
       end

julia> doh = Doh{Int64}();

So inside of the type declaration scope, Doh is like Doh{T} outside because T is already bound as a universal quantifier by the type block.

Patrick O'Leary

unread,
Apr 14, 2012, 1:55:51 PM4/14/12
to juli...@googlegroups.com
Makes sense. I read through the section in the manual a few times and tried it both ways, but when I left the type declaration off I ran into my other problem (have to specify the type parameter to the abstract type in the declaration, or the parameterized subtype relationship won't work) so I think I'm back on track. Thanks!

Stefan Karpinski

unread,
Jun 29, 2012, 11:55:13 AM6/29/12
to juli...@googlegroups.com
On Fri, Jun 29, 2012 at 11:40 AM, mikkelfj <mikk...@gmail.com> wrote:
 
abstract AXyzzy

type Xyzzy{X, Y} <: AXyzzy
xy::(X, Y)
function Xyzzy(x, y)
_ = new()
_.xy = (x, y)
return _
end
end

The idiomatic way to write this inner constructor is this following:

type Xyzzy{X,Y} <: AXyzzy
    xy::(X,Y)
    Xyzzy(x,y) = new((x,y))
end

The only reason you ever need to call new with too few arguments is if you have to mutate an object after creating it in order to completely initialize it. In this case, you definitely don't so you should just pass the one field to new.

mknoxyzzy(z::String) = Xyzzy(1,2)
mkxyzzy(x, y, z::String) = Xyzzy{typeof(x), typeof(y)}(x, y)
mkxyzzy(z::String) = Xyzzy{Int64, Int64}(1,2)

xyz = mkxyzzy(1, 2, "hello")
zyx = mkxyzzy("hello")
@assert xyz.xy == (1, 2)
@assert zyx.xy == (1, 2)


try 
mknoxyzzy("hello")
catch
println("noxyzzy failed as expected")
end

Not sure what you're trying to do here or what the confusion is so it's hard to help. You should probably read the chapter on constructors (again?). It's pretty complete and detailed although perhaps a little dense.

mikkelfj

unread,
Jun 29, 2012, 4:12:57 PM6/29/12
to juli...@googlegroups.com
The idiomatic way to write this inner constructor is this following:

type Xyzzy{X,Y} <: AXyzzy
    xy::(X,Y)
    Xyzzy(x,y) = new((x,y))
end

The only reason you ever need to call new with too few arguments is if you have to mutate an object after creating it in order to completely initialize it. In this case, you definitely don't so you should just pass the one field to new.

Thanks for explaining. Perhaps I am trying to cover too much ground too quickly - part of the reason to post here is to explain what caught my off guard, since it may be a pitfall for others. So I am not going to say how I think it should have been, only explain my reasoning - this can only be done once, while you are still new to the language.

First: yes, the documentation does cover the material, on a second read I went to composite types and methods and missed the separate constructors section, although there are clear references.

Second, I'm still left with a feeling things should work easier - here is my thinking:

There may be good reasons to feed new with arguments, but that shouldn't effect how to match a specific constructor, and in fact, it doesn't changing behavior if I say new((x, y)).

The reason behind the `mknoxyzzy` method is that it was my first attempt, and it didn't make a Xyzzy as I would have hoped. Noting that it would have worked if Xyzzy had no type parameters.

I realize there has to be some magic to derive the Xyzzy type parameter by inference from assignment to _.xy, but even if not, the constructor method missing is still confusing. The exact same parameterized signature works if used as a seperate non-constructor function, that is, if I added {X,Y} to mkxyzzy, that function would work. 

mkxyzzy{X, Y}(x::X, y::Y, z::String) = Xyzzy{typeof(x), typeof(y)}(x, y)

The same signature on an inner Xyzzy constructor still won't work unless the method is called with explicity type parameters.
type Xyzzy{X, Y} ...

        function Xyzzy{X, Y}(x::X, y::Y)
_ = new((x, y))
#_.xy = (x, y)
return _
end
end

Xyzzy(1,2)  results in  no method Xyzzy
Xyzzy{Int64, Int64}(1,2) results in Xyzzy instance.

So yes, I can add type parameters, but it seems a bit roundabout to enter typeof(x) etc., at least from my initial inexperienced approach, when the same thing works in other situations.

Now, for parameterized constructors without arguments this is more tricky, as the original post, but not really: If there is only one constructor of zero args, and it assigns a value to a parameterized type on the composite type, inference should be able to work. At least from intuitive thinking.

As to new() parameters, a minor thing is that it is less clear which members are initialized - for example, my tuple (x,y) looks like separate values x, y., and the constructors are prone to errors related to insertion of new fields.

Now, I really appeciate the type system of Julia for many reasons, just explaining what caused me some troubles.

Stefan Karpinski

unread,
Jun 29, 2012, 4:41:36 PM6/29/12
to juli...@googlegroups.com
Perhaps some confusion stems from the fact that inside of a type block the bare name of the type means the specific *parameterized* type object to be constructed. So writing Xyzzy{X,Y} inside of the type block is wrong (I'm not even sure what that does). Inner constructors are only called with type parameter explicitly specified, as in

Xyzzy{Int,String}(1,"hello")

As to the zero-argument outer constructor, which I presume you want to have called like this

Xyzzy()

what should the type parameters X and Y be? Since there are no values of x and y to derive the type from, you have to provide types somehow.

Stefan Karpinski

unread,
Jun 29, 2012, 4:48:18 PM6/29/12
to juli...@googlegroups.com
Here's a crack at what I think you want the behavior of this to be.

type Xyzzy{X,Y}
    xy::(X,Y)
    Xyzzy(x,y) = new((x,y))
end
Xyzzy{X,Y}(x::X,y::Y) = Xyzzy{X,Y}(x,y) # calls the inner constructor

Now the outer constructor will call the appropriate inner constructor based on the types of its arguments:

julia> Xyzzy(1,2)
Xyzzy((1,2))

Jeffrey Sarnoff

unread,
Jun 29, 2012, 5:18:01 PM6/29/12
to juli...@googlegroups.com
Stefan,
Why does this not store 7, and how does it need to be written to store 7?

  type Typ{T<:Integer}
     x::T
     Typ(x::T) = new(x)
  end

  Typ{T<:Integer}(x::T) = Typ(x+1)

  a = Typ{Int}(6)

Stefan Karpinski

unread,
Jun 29, 2012, 5:26:48 PM6/29/12
to juli...@googlegroups.com
There's an infinite recursion in the definition of your outer constructor — it calls itself. Thus, if you call it as Typ(6) it gives you a stack overflow. This is the correct definition:

Typ{T<:Integer}(x::T) = Typ{T}(x+1)

Then you get this:

julia> Typ(6)
Typ(7)

julia> Typ{Int}(6)
Typ(6)

The first one calls the outer constructor which calls the inner constructor with x+1; the second one is a direct call to the inner constructor, bypassing the increment.

Jeffrey Sarnoff

unread,
Jun 29, 2012, 5:48:37 PM6/29/12
to juli...@googlegroups.com

What are doing that I am not? After replacing the outer constructor:

julia> type Typ{T<:Integer}

            x::T
            Typ(x::T) = new(x)
         end

julia> Typ{T<:Integer}(x::T) = Typ{T}(x+1)

julia> a = Typ{Int}(6)
Typ(6)

Jeffrey Sarnoff

unread,
Jun 29, 2012, 6:03:04 PM6/29/12
to juli...@googlegroups.com
meant to write


  type Typ{T<:Integer}
     x::T
     Typ(x::T) = new(x)
  end

  Typ{T<:Integer}(x::T) = Typ{T}(x+1)

  a = Typ{Int}(6);

  a.x == 7 # false

Stefan Karpinski

unread,
Jun 29, 2012, 6:21:22 PM6/29/12
to juli...@googlegroups.com
Typ and Typ{Int} are not the same thing. You've defined them differently. You get different answers if you call one than the other.

Jeffrey Sarnoff

unread,
Jun 29, 2012, 6:38:08 PM6/29/12
to juli...@googlegroups.com
thank you

mikkelfj

unread,
Jun 30, 2012, 12:19:29 AM6/30/12
to juli...@googlegroups.com


On Friday, June 29, 2012 10:41:36 PM UTC+2, Stefan Karpinski wrote:
Perhaps some confusion stems from the fact that inside of a type block the bare name of the type means the specific *parameterized* type object to be constructed. So writing Xyzzy{X,Y} inside of the type block is wrong (I'm not even sure what that does). Inner constructors are only called with type parameter explicitly specified, as in

Xyzzy{Int,String}(1,"hello")
 
Yes, I tend to agree - this also why I do find parameterized constructors non-intuitive. But I would say, as inner constructor, type parameters would either be an error, or the types would relate to the arguments, not the type being created - otherwise it is not consistent with other methods. Which leads to the point, that perhaps inner constructors are better avoided - also seen answer to your other post.

As to the zero-argument outer constructor, which I presume you want to have called like this

Xyzzy()

what should the type parameters X and Y be? Since there are no values of x and y to derive the type from, you have to provide types somehow.

It is the same, Xyzzy() is a method, not a composite type, so it has 0 argument length to match the appropriate method. As to which type to construct, this (in my view) depends on how Xyzzy is implemented. If Xyzzy() = new(1, 2), then X, Y type parameters would be Int64 or Int32.

mikkelfj

unread,
Jun 30, 2012, 12:25:06 AM6/30/12
to juli...@googlegroups.com


On Friday, June 29, 2012 10:48:18 PM UTC+2, Stefan Karpinski wrote:
Here's a crack at what I think you want the behavior of this to be.

type Xyzzy{X,Y}
    xy::(X,Y)
    Xyzzy(x,y) = new((x,y))
end
Xyzzy{X,Y}(x::X,y::Y) = Xyzzy{X,Y}(x,y) # calls the inner constructor

julia> Xyzzy(1,2)
Xyzzy((1,2))
Now the outer constructor will call the appropriate inner constructor based on the types of its arguments:

Yes, this indeed close to what I want. In this case the inner constructor seems almost to be unnecessary, but it does create a tuple.
However, it is not clear that the inner constructor is called and not a recursive call to the outer constructor. In fact, given the exact types to the constructor, that would be a better match.

Further, now we have confusion with same method notation {X, Y} meaning two different things: the inner constructor is parameterized by constructed type, the outer by arguments.

So I still find it confusing, but I recognize it is a tricky problem.

mikkelfj

unread,
Jun 30, 2012, 1:18:51 AM6/30/12
to juli...@googlegroups.com
One proposal that springs to mind is to disallow inner constructors. As it is, you can also have other methods inside composite types, and it is even less clear what they do, especially with respect to type parameters.

So instead you would only have outer constructors, and they really are just ordinary methods, no more, no less.
To create a composite type you use same notation as `convert`, which effectively also creates new objects.
Here using create instead of new:

Xyzzy(x, y) = create(Xyzzy, (1,2))
Xyzzy() = create(Xyzzy, (Nothing, Nothing)
mkxyzzy(z::String) = create(Xyzzy, 1, 2)

There isn't really a way to define the create function without getting into issues of recursion calling the outer or inner constructors.
But it could be done internally similar to new.

We might also want to be able to have partially created types similar to new() and assign members subsequently. This could be done with:

Xyzzy() =  (_ = create(Xyzzy); _.xy = (Nothing, Nothing))

mikkelfj

unread,
Jun 30, 2012, 1:21:38 AM6/30/12
to juli...@googlegroups.com
Oh, forgot.
Type parameters are then given as:

Xyzzy() = create{String, String}(Xyzzy, ("there is nothing here", "nor here"))

mikkelfj

unread,
Jun 30, 2012, 1:27:21 AM6/30/12
to juli...@googlegroups.com
But this proposal leads to another problem:
How to deal with default constructors since they are only created if no other constructors are created, which you can never know.
One solution is to this is to always have default constructors, but let the be shadowed by either any outer constructors seen, or only shadowed by outer constructors with a matching signature. This is not so different from today, but we avoid having inner constructors, and create(...) provides access to shadowed default constructors.

mikkelfj

unread,
Jun 30, 2012, 5:34:31 AM6/30/12
to juli...@googlegroups.com
OK, I have a better understanding now:

anything inside a parameterized composite type block uses those type parameters.
The constructor is really no different from other methods, and hence there can be other methods inside such a block.

type Foo{X, Y}
  x::X
  y::Y
  Foo(x::X) = new(x, 0)
  bar(z::(X, Y)) = new(z[1], z[2])
  baz{Z}() = println("does it makes sense to allow a type parameter like Z here?")
#...
end

I still find it mildly confusing with inner constructors, but I can see how you can write a class of parameterized functions easily that way.

My problem has nothing to do with constructors, but with my expectations of type inference.

As I said earlier:

  Xyzzy() = new((1,2))

ought to be able to figure out the parameterized types from the implementation, here new((1,2)), but it can't.

Stefans proposal for outer to inner constructor works. the recursion I mentioned was wrong, since the inner constructor matches on the tuple type.

However, I find myself running into problems everywhere, not specifically constructors, where I have to declare type parameters explicitly. And it spreads to functions calling these functions when the function argument does not directly imply the type parameters.

I am most likely going about this the wrong way to end up in this situation in the first place.

Stefan Karpinski

unread,
Jun 30, 2012, 12:10:47 PM6/30/12
to juli...@googlegroups.com
I'm not going to respond to all of this but I'll make a couple of points. The *only* way to construct composite objects is by calling `new`, which is only available to methods declared inside of the type block. So the proposal of doing away with inner constructors makes no sense — it's equivalent to saying "let's just not make composite objects at all".

There is a huge different between a zero-argument constructor call and a two-argument constructor call: in the former, there are no objects to stick into the pair for the field, and therefore nothing that you can take the type of to even determine what kind of object you're trying to construct. I think you're about "type inference" the wrong way here. This isn't type inference — it's having objects whose type implies the values of type parameters. The only place where

Xyzzy() = new((1,2))

makes sense is inside the type block since otherwise `new` isn't visible. (What type would new construct outside of a type block? Inside a type block we know to constrict *this* type; elsewhere there's no "topic type", so what does it make?)

If you were to add a default outer constructor like this:

Xyzzy() = Xyzzy(1,2)

then, indeed, there is enough information to make an object — because you have the actual x and y values 1 and 2. So Xyzzy() creates an Xyzzy{Int,Int} object.

I'm not sure what you mean by type parameters spreading to calling functions. That doesn't make much sense because there's no way to pass a type paramater at function call time. The type parameters are all determined implicitly from the types of arguments (except in the case of singleton-type arguments, in which case the argument itself can determine the type parameter).

I don't think the phrase is mentioned in the manual anywhere, but Jeff and I refer to method declarations with signatures like this:

foo{T<:Integer}(x::T, y::T) # method A

as "diagonal methods" or "diagonal signatures" as compared to something like this:

foo(x::Integer, y::Integer) # method B

The name is due to the fact that it allows dispatch to apply down the diagonal of a type table:

 x,y  Int16 Int32 Int64
Int16   A     B     B
Int32   B     A     B
Int64   B     B     A

This is the kind of thing that type parameters on method declarations are used for. Type parameters on types are a little different since they give specializations of parametric types to specific parameter values. So the declaration foo{T}(x::T,y::T) is very different from the call foo{T}(1,2): the former is a declaration of a diagonal method (T is shadowed if it exists), whereas the latter only makes sense if foo is a type and T has to be bound to something to supply as a type parameter.

mikkelfj

unread,
Jun 30, 2012, 5:48:21 PM6/30/12
to juli...@googlegroups.com
Again thanks for your patience.

makes sense is inside the type block since otherwise `new` isn't visible. (What type would new construct outside of a type block? Inside a type block we know to constrict *this* type; elsewhere there's no "topic type", so what does it make?)

My proposal in summary was a have a function

  create{TypeParams}(T, args...), where TypeParams are optional

which was supposed to be equivalent to the default constructor T(args...) in:

type T{TypeParams}
  T(args...) = new(args...)
end

As mentioned, I'm not so sure it is such a good idea anymore. But it does make `new` functionality available to outer constructors, and to any other function unambigiously. But it also makes it possible for users to create types in a way they were not intended for.


I'm not sure what you mean by type parameters spreading to calling functions.

It is about notation spreading in source code: Foo{X, Y}(a::X, b::Y = Xyzzy{X, Y}(a + a, b + b)  There are situations where it becomes difficult to avoid having all these {X,Y} type parameters everywhere. Otherwise you get method missing a lot, and not always for obvious reasons.

The solution appears to be to implement logic in abstract base classes and to use a few concrete accessor methods, rather than trying to implement generic functionality on concrete types that are parameterized.
 
I don't think the phrase is mentioned in the manual anywhere, but Jeff and I refer to method declarations with signatures like this:

foo{T<:Integer}(x::T, y::T) # method A

as "diagonal methods" or "diagonal signatures" as compared to something like this:

Makes sense :-)

This is the kind of thing that type parameters on method declarations are used for. Type parameters on types are a little different since they give specializations of parametric types to specific parameter values. So the declaration foo{T}(x::T,y::T) is very different from the call foo{T}(1,2): the former is a declaration of a diagonal method (T is shadowed if it exists), whereas the latter only makes sense if foo is a type and T has to be bound to something to supply as a type parameter.

Yes, I can see these examples, it makes for very powerful computation. Its just than when get yourself into a style the language is not intended for, it becomes confusing with method missing. I just rewrote some code to work more on abstracts with much less friction.


Jeffrey Sarnoff

unread,
Jun 30, 2012, 6:02:19 PM6/30/12
to juli...@googlegroups.com
"  Its just than when get yourself into a style the language is not intended for, it becomes confusing  " (no doubt)

With your current perspective, would you briefly share the "style", the approach to setting up your abstractions and realizations, 
that you found did not fit, and note what for you was the most important initial misunderstanding/misdirection including a sense
of the more workable approach you decided upon.  Having that simply stated may save someone else the same consternation. 

thanks

mikkelfj

unread,
Jul 1, 2012, 1:41:24 AM7/1/12
to juli...@googlegroups.com


On Sunday, July 1, 2012 12:02:19 AM UTC+2, Jeffrey Sarnoff wrote:
"  Its just than when get yourself into a style the language is not intended for, it becomes confusing  " (no doubt)

With your current perspective, would you briefly share the "style", the approach to setting up your abstractions and realizations, 
that you found did not fit, and note what for you was the most important initial misunderstanding/misdirection including a sense
of the more workable approach you decided upon.  Having that simply stated may save someone else the same consternation. 

Hard to summarize, but in Julia you have this challenge that you must carry a type to avoid namespace pollution:


mergedata(as::AbstractStorage, xdata::AbstractData, ydata::AbstractData) = publish(as, merge(as, xdata, ydata))

or just

mergedata(as::AbstractStorage, xdata, ydata) = publish(as, merge(as, xdata, ydata))

Then implement abstract defaults of publish and merge, possibly raising exceptions, and hand over to concrete types.
Here I mostly use AbstractStorage as a module name and a vehicle for specialization, only secondarily as a container to carry a container to write to.
Note that strictly, merge did not need the as argument, but sometimes ydata and xdata are functions returning functions that cannot easily be matched on type. Thus it helps to have "module" as first argument, similar to class methods elsewhere.

vs the original approach


mergedata{T, D}(st::MyStorage{T}, xdata::D, ydata::D) = publish{T, X}(st::T, merge{T, D}(xdata, ydata))

It wasn't that I wanted all these type annotations, they just became necessary along the way, partially while trying to ensure xdata and ydata types are identical, which is difficult in the abstract case. It turns out not to be worthwhile, and data can wrong in other ways such as wrong container, and arguments may be function closures that cannot easily be type matched. So the abstract version works much better, and you can always specialize a function like merge to do additional checking if needed.

But why I have to add so many type annotions, and not have the type system infer most, is still puzzling me, possibly because I've worked with OCaml where all types are painfully detailed up front, even when you don't type them.


Stefan Karpinski

unread,
Jul 1, 2012, 11:39:14 AM7/1/12
to juli...@googlegroups.com
This doesn't make sense:

mergedata{T, D}(st::MyStorage{T}, xdata::D, ydata::D) = publish{T, X}(st::T, merge{T, D}(xdata, ydata))


When you write publish{T, X} in anything but the declaration of a method, the {...} means that you are treating publish as a parametric type and applying parameters to that type. Also, writing st::T in the method definition asserts at run-time that st is of type T.

When you are declaring a method, there are only two reasons you might want to use type parameters:
  1. you need "diagonal dispatch"
  2. you need to refer to the type of one of the arguments in the method body
In this case, if you need xdata and ydata to have the same type, you would write the above like so:

mergedata{D}(st::MyStorage, xdata::D, ydata::D) = publish(st, merge(xdata, ydata))

I kind of suspect your original profusion of type parameters was largely due to misunderstanding how to use them. As I said, you should only ever need type parameters on methods when doing diagonal dispatch or using the types of arguments method bodies.
Reply all
Reply to author
Forward
0 new messages