Type() vs type() -- Are constructor methods or conversion functions more idiomatic?

I am writing some code with structs that represent dual forms of the same underlying thing, e.g.

struct NormalForm
    normal_data
    descriptive_parameter
end
NormalForm(data) = NormalForm(data, process(data))

struct DualForm
    dual_data
    descriptive_parameter
end
DualForm(data) = DualForm(data, process(data))

I also have functions which convert between them:

normal_form(df::DualForm) = NormalForm(lambda(df))
dual_form(nf:NormalForm) = DualForm(lambda(nf))

My instinct was to write the conversion functions as distinct functions like this, rather than as new methods for the type constructors. But since they do the same thing that a constructor method would, it has occurred to me that they could just as easily be constructor methods. But should they?

The built-in struct String is similar – there is a constructor String(), and there is the conversion function string(). Both return objects of type String. So why is string() defined separately?

On the other hand, there is not an int() function. If I want to convert another numeric type to an integer, I have to call the type’s constructor, Int().

The docs suggest that conversion functions should not create new objects if they don’t have to, while constructors always should. That might explain the difference between String and Int, and may suggest that the constructor approach is correct here, since I’ll never call dual_form on a DualForm object?

Defining e.g. Base.convert(::NormalForm, x::DualForm) would work, but it seems like that’s really only designed for implicit conversions rather than explicit ones like I want here.

So what is the preferred style for writing a function which returns objects of user-defined types? Is a constructor method more idiomatic (as in Int()) or are distinct conversion functions (as in string())?

5 Likes

I’ve encountered a similar situation in the past where my code basically consisted of a bunch of constructors. But it felt weird. You need some verbs in there. What if you have one function (with two methods) called flip or conjugate or dual that turns a normal form into a dual form and vice versa?

I guess I would say the rule of thumb is that a constructor just assembles data with a minor amount of processing (along with enforcing invariants). As you mentioned, convert is meant primarily for the implicit conversion that Julia does inside default constructors and, e.g., when assigning elements of vectors.

2 Likes

I agree, in Julia it makes more sense to use a constructor for the simple instantiation of a particular type, and leave the processing to a regular function. This works better for composability.

Imagine someone makes another package to cover some special case of dual forms, that needs different concrete types SpecialNormalForm and SpecialDualForm. They can overload your normal_form and dual_form functions so users can use the same functions to write generic code that works for both NormalForm and SpecialNormalForm. This would not make sense with constructors (calling DualForm(x::SpecialNormalForm) to make a SpecialDualForm instead of a Dualform, which is technically possible but discouraged).

1 Like

I am not sure there is a consensus about this, but I try to avoid exposing type constructors in the API, and do

export make_foo

make_foo(args...) = Foo(args...)

even where no other validation or calculation is required. Where the make_ distinguishes the function from the potential variable name so I get to use foo for the latter.

I find this extra layer works nice for refactoring and deprecations, which happen almost surely eventually.

3 Likes

I use the following convention

  • convert(Foo, x) creates a Foo from exactly one input, x via something that a human would call “conversion”. If might be: convert(::Type{Foo}, x) = x isa Foo ? x : Foo(x)
  • Foo(x, y, z) creates a Foo, but not necessarily by “converting” anything (e.g. Vector{Int}(undef, 3).)
  • foo(x) might create a Foo, but it might also create a DifferentFoo, depending on what x is.
1 Like