associating outer constructors with different names with a type

199 views
Skip to first unread message

Uri Patish

unread,
Feb 5, 2016, 4:16:24 PM2/5/16
to julia-users
Hi,

I was wondering what is the best way going about the following issue. Suppose I have a type with few outer constructors which I name differently, and I want Julia to recognize these functions as constructors of that type. For example

type SomeType <: SomeAbstract
  name::ASCIIString
  value::Int
end

TypeA(value::Int) = SomeType("A", value)
TypeB(value::Int) = SomeType("B", value)

In this case, I have two pseudo-types, TypeA and TypeB, which have the same structure and are only different by their state. The problem with this approach is that now TypeA and TypeB are functions and not datatypes, thus, I cannot use operations defined on datetypes like checking for subtyping (TypeA <: SomeAbstract), or overloading type construction using call. 

Clearly, I could have defined two types which have exactly the same structure but this seems wrong to me as it involes code replication. Another possible solution is to define a macro which defines the type and its outer constructor, and I would call the macro to define TypeA and TypeB. Nonetheless, I was wondering if there was a way to tell Julia that the functions TypeA and TypeB are in fact constructors of SomeType. I'd be glag to hear about other solutions as well. 

Uri


Christopher Alexander

unread,
Feb 5, 2016, 5:03:42 PM2/5/16
to julia-users
Instead of having the name be ASCIIString, you could have that be another type like SomeTypeName, (which then has the string "A" or "B" as an attribute).  Then you could do a type alias to map TypeA to SomeType{SomeTypeA} and TypeB to SomeType{SomeTypeB} assuming SomeTypeA and SomeTypeB are types that extend an abstract type SomeTypeName.

For example, I have something like this in one of my projects:

abstract ModelType
abstract Model

# model types
type AffineModelType <: ModelType end
type TermStructureConsistentModelType <: ModelType end

# type aliases
typealias AffineModel Model{AffineModelType}
typealias TermStructureConsistentModel Model{TermStructureConsistentModelType}

Someone else feel free to chime in if this isn't really the best way, as I'm still learning too!

Chris

Uri Patish

unread,
Feb 6, 2016, 5:52:29 AM2/6/16
to julia-users
Hi Chris, thanks for weighing in ;)

The solution you suggest is indeed a sound one, and one of my attempts at this issue was through parametric types and an enum instead of attributes like you suggest, e.g., 

@enum TypeKind A=1 B=2

type SomeType{T}
  value::Int
end

typealias TypeA SomeType{A}
typealias TypeB SomeType{B}

What I didn't like about this is solution is that only the types' names are parametric, but the types' bodies are exactly alike. In other words, it feels wrong to me to say that two types are different even though they are only different in the kind of instances they produce. Instances of types can be different in state, but if their structure is the same, then I feel that the best approach should indicate this by relying on a single type, and producing the different instances by overloading the constructor. 

BTW, going forward with the multiple types approach, the solution I found as most convenient is using a macro. This goes something like the following, 

macro sometype(typename, typevalue)
  quote
    type $(esc(typename))
      name::ASCIIString
      value::Int
    end
    $(esc(typename))(value::Int) = $(esc(typename))($typevalue, value)
  end
end

@sometype TypeA "A"
@sometype TypeB "B"

But I was hoping that maybe there was a solution using the single type approach.

Uri

Uri Patish

unread,
Feb 6, 2016, 6:24:15 AM2/6/16
to julia-users
Minutes after I posted the last reply, the solution presented itself to me, so I'll write it here for posterity ;)

The missing link was indicating that the type has different names, thus rendering the different functions as constructors. Following the running example in this thread, this idea turns out as follows,

type SomeType
  name::ASCIIString
  value::Int
end

typealias TypeA SomeType
TypeA(value::Int) = SomeType("A", value)

typealias TypeB SomeType
TypeB(value::Int) = SomeType("B", value)

Now, when testing if TypeA <: SomeAbstract returns true

If anyone has thoughts about the different approaches, I'd love to hear.

Uri

On Friday, February 5, 2016 at 11:16:24 PM UTC+2, Uri Patish wrote:

Uri Patish

unread,
Feb 6, 2016, 7:53:35 AM2/6/16
to julia-users
It applear my last solution doesn't work as is, since the second type alias overwrites the first. The workaounrd I found is the making the second alias to the first typealias, i.e., 

typealias TypeA SomeType
TypeA(value::Int) = SomeType("A", value)

typealias TypeB TypeA
TypeB(value::Int) = SomeType("B", value)

I think this might be the result of the way the Julia parser holds typealiases, but can't say if this is a matter of design or a bug. I'll post a different thread on the matter. 

Uri

Yichao Yu

unread,
Feb 6, 2016, 8:12:16 AM2/6/16
to Julia Users
On Sat, Feb 6, 2016 at 5:52 AM, Uri Patish <urip...@gmail.com> wrote:
> Hi Chris, thanks for weighing in ;)
>
> The solution you suggest is indeed a sound one, and one of my attempts at
> this issue was through parametric types and an enum instead of attributes
> like you suggest, e.g.,
>
> @enum TypeKind A=1 B=2
>
> type SomeType{T}
> value::Int
> end
>
> typealias TypeA SomeType{A}
> typealias TypeB SomeType{B}
>
> What I didn't like about this is solution is that only the types' names are
> parametric, but the types' bodies are exactly alike. In other words, it
> feels wrong to me to say that two types are different even though they are
> only different in the kind of instances they produce. Instances of types can
> be different in state, but if their structure is the same, then I feel that
> the best approach should indicate this by relying on a single type, and
> producing the different instances by overloading the constructor.

So for you, `type A a::Int end` and `type B a::Int end` shouldn't be
different type?

Your type alias approach won't work since there's no difference
between different names at all.

Uri Patish

unread,
Feb 6, 2016, 8:31:58 AM2/6/16
to julia-users
Yes, 'type A a::Int end' and 'type B a::Int end' should be just a single type. 

Indeed you are right, in a second trial I've done the typealias solution didn't work as the second method definition overwrites the first. 

Yichao Yu

unread,
Feb 6, 2016, 9:41:57 AM2/6/16
to Julia Users
On Sat, Feb 6, 2016 at 8:31 AM, Uri Patish <urip...@gmail.com> wrote:
> Yes, 'type A a::Int end' and 'type B a::Int end' should be just a single
> type.

I think that's not the right understanding of (julia) types. There are
many more informations a type can carry other than the data layout and
depending on what you need those information for, they don't
necessarily have to live in the memory of the object. It will also be
a waste of memory to store these shared information in every types
since it will be exactly the same for all instances of the type.

Uri Patish

unread,
Feb 6, 2016, 10:34:40 AM2/6/16
to julia-users
I agree that with the case of a string field my approach would be more exprensive memorywise, but this is not necessarily so in the general case. For example, having a type whose invariant is that it only holds integers within a certain range, and having different outer constructors for each range. In this example, the parametric type aproach would cost more bits. 

Regarding your note on how types should be used, I tend to disagree. For example, in languages that support OOP and allow overloading, it is considered a bad practice to have two classes that are exactly alike, in order to differentiate between groups of instances. Even though Julia has multiple dispatch rather than single dispatch (traditional OOP), I don't think this should result in different design patterns. 

Uri

Kristoffer Carlsson

unread,
Feb 6, 2016, 2:09:52 PM2/6/16
to julia-users
A dual number and a complex number are two completely different things even if they contain the same data. Why would it be bad practice to separate these into different types?

Stefan Karpinski

unread,
Feb 6, 2016, 2:10:26 PM2/6/16
to julia...@googlegroups.com
Many Julia types are just 64 bits of data – Int, UInt, Float64. Should those all be a single type?

Toivo Henningsson

unread,
Feb 7, 2016, 9:15:11 AM2/7/16
to julia-users
I think rather the issue here is that SomeType instances should behave more or less the same no matter whether they were constructed through TypeA or TypeB, and it would be wasteful for the compiler to specialise code on these two cases separately?

To try to answer the question from this point of view though: Julia does not provide a way to override <: , you would have to create your own function for the purpose of being able to make this distinction. With the new function overhaul though, it will be possible to overload it sparsely separately for an argument TypeA and TypeB, though.

Uri Patish

unread,
Feb 8, 2016, 5:29:16 AM2/8/16
to julia-users
Hi Stefan, Toivo. Thanks for joining the discussion. 

Toivo you are on point. Having though more on the matter, I think the right to describe the sitution is a s follows. Suppose we have two immutable types whose data layout is exactly the same, their behavior is exactly the same, and the only difference between them is their invariant which is enforced through two different constructors. In this case, I think it would be wrong to hide this information both from the compiler, and from other programmers. Even though I'm no expert in compilation, seems to me that providing this additional information about the type equivalence to the compiler can only benefit the optimization that happen under the hood. Second, from the programmer perspective, I would say that emphasizing that two types are only different in their constructors increases code readiability. For someone whose reading the code, having one less type to remember makes things easier. It makes the type hierarchy more parsimonious.

From an information theoretic perspective, defining two different types in case denies any reciever of the code (compiliers or other programmers) the fact that they are equivanlent. I cannot see any way how this could be beneficial. 

In practice, I think the right way to go about this is to define a single type, and have few overloaded constructors. In Julia, this can be achieved by adding another parameter of value type. The only problem with this solution is that this makes the constructor a little more cumbersome to use. To overcome this, I can see two solutions. The first, which I like less, is to have a constructor alias. In this case, Julia would know to associate other constructors with the type, thus, allowing the use of <:, or any other method that operates on datatypes with the other constructors. The second solution, which I prefer, is adding or changing type aliases to type references. The problem with type aliases is that they merely provide shorthand notation, which I assuming, is removed by the Julia parser. If on the other hand, one could define a type reference which would indicate that the new type has exactly the same data layout as another type, but allowing the dispatch system to distiguish between the two types in terms of method calls, then this situation would have a great solution. The code in case would look like,

immutable SomeType
  value::Int
end

typeref SomePositiveType SomeType
function SomePositiveType(value::Int)
  if value < 0
    error("value must be positive")
  end
  SomeType(value)
end

typeref SomeNegativeType SomeType
function SomeNegativeType(value::Int)
  if value > 0
    error("value must be negative")
  end
  SomeType(value)
end

If type references were an option, following the last discussion, then my answer to Tim and Stefan would be yes. Int, UInt, and Float64 should all be type references to a more basic type, maybe Number64. 

Uri

Mauro

unread,
Feb 8, 2016, 5:56:49 AM2/8/16
to julia...@googlegroups.com
On Mon, 2016-02-08 at 11:29, Uri Patish <urip...@gmail.com> wrote:
> immutable SomeType
> value::Int
> end
>
> typeref SomePositiveType SomeType
> function SomePositiveType(value::Int)
> if value < 0
> error("value must be positive")
> end
> SomeType(value)
> end
>
> typeref SomeNegativeType SomeType
> function SomeNegativeType(value::Int)
> if value > 0
> error("value must be negative")
> end
> SomeType(value)
> end
>
> If type references were an option, following the last discussion, then my
> answer to Tim and Stefan would be yes. Int, UInt, and Float64 should all be
> type references to a more basic type, maybe Number64.

If I understand you correctly, being able to make typeref's has been
discussed, maybe of interest:
https://groups.google.com/d/msg/julia-dev/2tMXJbggKO8/RQ2H_55VLOkJ
https://groups.google.com/d/msg/julia-dev/v8B1tI_NB5E/tk98D0iKopYJ

Uri Patish

unread,
Feb 10, 2016, 10:05:46 AM2/10/16
to julia-users
Hi Mauro, 

Thanks for the links! I'll have a look. 

Uri

Reply all
Reply to author
Forward
0 new messages