Non-constructor functions inside struct

I was trying to define a function which has access to new, but is different from the constructor. Consider this MnWE:

julia> struct Foo
       x::Int
       bar(x::Int) = new(x)
       end

julia> methods(bar)
ERROR: UndefVarError: bar not defined
Stacktrace:
 [1] top-level scope at none:0

julia> VERSION
v"1.0.0"

I was surprised that bar is just silently ignored. Is there an issue for this?

1 Like

BTW, I think the correct way of doing this is

function bar end

struct Foo
    x::Int
    global bar(x::Int) = new(x)
end
1 Like

Interesting. How is this useful?

I have a type for which I want to validate the fields, and also provide a function that just returns nothing for invalid input.

Assume that

  1. isvalid below is costly, so I don’t want to call it superflously,
  2. I don’t want to provide an argument to the constructor that tells not to validate (this is a standard approach I have seen, but I have seen it get abused),
  3. I always want the constructor Foo to return a ::Foo, so I need a separate function.
function valid_or_nothing end

struct Foo
    x                           # validated
    function Foo(x)
        @assert isvalid(x)      # this is assumed to be costly
        new(x)
    end
    global valid_or_nothing(::Type{Foo}, x) = isvalid(x) ? new(x) : nothing
end

I would appreciate more input on whether is approach (non-constructor functions inside the type) is OK, then if there is interest, it would make a PR to the documentation.

Here is a use cases in base:

2 Likes

I have another situation where being able to define a function (the isvalid function below) inside the struct definition would be helpful. I want to create a parametric struct S but I want all constructors to explicitly specify parameters (so no S(x) and no inferring parameters from arguments).

struct S{T}
isvalid(a)::Bool = ... # imagine lots and lots of checks. too much to repeat for each constructor.
S{a}() where {a} = isvalid(a) ? new{a}() : error("S{$(a)} cannot be created")
S{b}() where {b} = isvalid(b) ? new{b}() : error("S{$(b)} cannot be created")
end

I think it would be nice to not have to define function isvalid end outside the struct. And would certainly be nice for the code above to give an error if function isvalid end is not already created. (It does not as of Julia 1.9.0)

I am curious to hear people’s thoughts on this.

You can, you just can’t affect the global scope from a struct scope by default, you need a global declaration. In the original example and your case, isvalid should be defined in the global scope. After all, if you need to add a method to a global function, you should generally do it in the global scope. The original examples had an exception because global valid_or_nothing needed to access new for instantiation. It was probably better to refactor so that instantiation never skips a Foo call; a Foo method itself can optionally skip validation steps to new.

Example of a function isolated to a struct’s local scope:

julia> begin
       foo() = 1 # global foo
       struct X
         foo() = new() # struct-local foo
         X() = foo()   # uses local foo
       end
       X(), foo()
       end
(X(), 1)
2 Likes

I see. So I was trying to access/test my _isvalid function from outside the struct. But If I want that, I need to declare it a global. That make sense.

struct A
global _isvalid() = true
end
@assert _isvalid()

But yeah, being able to access new from non-constructor functions would save me repetition:

_isvalid(x) = ...
struct S{T}
    _S(a) = _isvalid(a) ? new{a}() : error("S{$(a)} cannot be created")
    S{x}() where {x} = _S(x)
end

This is if I really want to not have an S constructor and only S{T}.

I read the above posts again, and I see that it is possible to do the following (adding the global declaration)

_isvalid(x) = ...
struct S{T}
    global _S(a) = _isvalid(a) ? new{a}() : error("S{$(a)} cannot be created")
    S{x}() where {x} = _S(x)
end

But again, that means there is now a _S function in the global scope that constructs S{T}. So if we’re obsessed with limiting the constructors to S{T} (maybe not a justifiable obsession), this is not a solution.

1 Like

I agree that is a problem. But a local _S can already access new, so your example without the global declaration should work as well. _isvalid is global so you can test it as you need. I’ll amend my earlier example to demonstrate.

I would still prefer making constructors that rely only on each other and new because fewer functions is simpler. For example if I had a struct X{A,B,C} but only want to expose X{A} and X{A,B}, I would likely make outer X{A} forward to inner X{A,B} which uses new{A,B,C}. Of course, your real use case is probably more complicated than your examples, so if inner constructors using a local non-constructor function make things simpler, then go for it.

1 Like

You’re right. You’d already given me a solution. I was confusing myself. I guess in the original post the author did want something in the global scope.

I’m going off topic now just to share a bit about what you’ve been helping me out with: I have these different functions to take some input and do transformations. The parameters of these functions are experimentally determined. I basically want to store the parameters as fields of a struct create functors out of them. So I don’t want users to be able to create new types, just make instances of the ones available.

Basically I have Transformer{T}, and want to restrict what Ts are allowable. I can check that T is of acceptable values inside the constructor before returning. That would be the typical way to do it. But I wanted to see if I can do it with dispatch rather than if/else.

1 Like