Constructors in Julia: initializing named fields based on the input value of other named fields

891 Views Asked by At

Imagine a constructor that takes two arguments and initializes 3 named fields using the values of the two arguments. Something like this:

type test1
   a
   b
   c
   test1(a,b) = new(a,b,a/b)
end

This works fine, but what if the value for c is not such a simple expression? What if it runs over a line or two? Or is a complex list comprehension? Sticking the expression for c directly into the new() is unwieldy and makes the code harder to read (IMO). I'd rather do something like this:

type test1
   a
   b
   c = a/b
   test1(a,b) = new(a,b,c)
end

but a and b aren't defined until the call to test1(a,b) apparently, so this doesn't work. Perhaps I'm just looking for syntactic sugar. In any case, I'd like to understand better when the values of the arguments of the constructor become known and if they can be used before the call to new().

Is there a better way (better than the first example) to do what I'm attempting to do in the second example?

(I think the following question and its answers are related enough to be helpful, but I'm still too much of a Julia newbie Building a non-default constructor in Julia)

Edited: At the risk of being too specific, I thought I'd include the actual use case where this question arose. I'm doing an adaptive integration scheme. Each volume element that straddles the integration boundary is further subdivided. My definition of the "cube" type is below. My student wrote a working prototype in python, but am trying to rewrite it in julia for the performance gain.

using Iterators
# Composite type defining a cube element of the integration domain
type cube
    pos # floats: Position of the cube in the integration domain
    dx  # float: Edge length of the cube
    verts # float: List of positions of the vertices 
    fvals::Dict # tuples,floats: Function values at the corners of the cube and its children
    depth::Int # int: Number of splittings to get to this level of cube
    maxdepth::Int # Deepest level of splitting (stopping condition)
    intVal # float: this cube's contribution to the integral

    intVal = 0

    cube(pos,dx,depth,maxdepth) = new(pos,dx,
           [i for i in product(0:dx:dx,0:dx:dx,0:dx:dx)],
           [vt=>fVal([vt...]) for vt in [i for i in product(0:dx:dx,0:dx:dx,0:dx:dx)]],
           depth,maxdepth,intVal)
end
2

There are 2 best solutions below

1
On BEST ANSWER

Inner constructors are just the functions that appear inside the type block with the same name as the type. That means that you can use either function syntax to define them. You can use the short form (as you did above), or you can instead use the more verbose block syntax:

type test1
   a
   b
   c
   function test1(a,b)
      c = a/b
      return new(a,b,c)
   end
end

The call to new doesn't even need to be the last expression in the method; you can assign its result to an intermediate variable and then return it.


A few more details: The type block is just like a normal Julia scope with a few exceptions:

  • Any symbols that appear alone or with a type annotation become the fields of the type.
  • Functions have access to the special new builtin to instantiate the type, and functions with the same name as the type become the inner constructor.

When you put any other assignments inside the type block, they simply create a local variable within that scope. It's not a field or a part of the type, but simply a variable that can be used in the constructor's methods. That said, it's not very useful and will likely change in the future.

0
On

Using Matt B.'s answer I constructed the following answer for my specific use case. Using the block syntax for function is a lot cleaner.

using Iterators
# Composite type defining a cube element of the integration domain
type cube
    pos # floats: Position of the cube in the integration domain
    dx  # float: Edge length of the cube
    verts # tuple: List of positions of the vertices 
    fvals # floats: Function values at the corners of the cube and its children
    depth::Int # int: Number of splittings to get to this level of cube
    maxdepth::Int # Deepest level of splitting (stopping condition)

    function cube(pos,dx,depth,maxdepth)
        verts = [pos+[vt...].*dx for vt in product(0:1,0:1,0:1)]
        fvals = [fVal([vt...]) for vt in verts ]
        return new(pos,dx,verts,fvals,depth,maxdepth)
    end
end