Can I “couple” two types?

question

#1

This might be a quite specific question, however it came up in the things I plan to do. Assume you have two abstract types

abstract A1
abstract A2

and they like to appear together, but not always (so its not a composite or something, they are just, let’s say related), i.e. there is a general +(a::A1, b::A2)::A1 defined for example, but, to be precise, they only appear (or make sense) for “equally coupled“ subtypes

if you have for example

type B1 <: A1 # ...somehow defined with fields, the same for the next 3
type B2 <: A2
type C1 <: A1
type C2 <: A2

then for example the + from above only makes sence for each couple of types B1,B2 and C1,C2 but not for B1,C2 fro example. While several functions are hence only defined really specifically for B or C there might be a function that can be writte quite general, for example as

function quiteGeneral{T <: A1, S <: A2}(p::T, q::Vector{S})::T

now my quesion is:
Is there a way of typing to say/define something like
If T is B1 then S has to be B2 (if T is C1 then S is C2) while keeping just this one definition of this quite General function (and not implementing each type separately)?


#2

One approach (with abstract types) is to use an accessory function:

do_quiteGeneral{T <: A1, S <: A2}(p::T, q::Vector{S})::T = # accessory function, calculation goes here
quiteGeneral{T <: B1, S <: B2}(p::T, q::Vector{S}) = do_quiteGeneral(p, q)
quiteGeneral{T <: C1, S <: C2}(p::T, q::Vector{S}) = do_quiteGeneral(p, q)
quiteGeneral{T <: A1, S <: A2}(p::T, q::Vector{S}) = error("incompatible types") # fallback method

Depending on your case, you may be able to simplify it like this:

do_quiteGeneral(p::A1, q::Vector{A2})::A1 = # accessory function, calculation goes here
quiteGeneral(p::B1, q::Vector{B2}) = do_quiteGeneral(p, q)
quiteGeneral(p::C1, q::Vector{C2}) = do_quiteGeneral(p, q)
quiteGeneral(p::A1, q::Vector{A2}) = error("incompatible types") # fallback method

#3

Thank you for your answer. That was, what I also thought and the one point I think is not so nice for that is, that I might have maybe 12 of such „couples of subtypes“ and the modle should enable programmers to easily extend the module with new ones. In that scenario I don’t want to add two lines for every quiteGeneral function just to kind of enable the certain combination.

Short: Yes that is a solution, but I think it is too strict or too complicated for my use case. And don’t get me wrong, maybe there is no solution for my use case, which would just mean that the error message will appear in one of the not so general functions which is then nonexistent for the couple of subtypes somebody tries to use. So this is really mainly to provide a suitable or nicer error message and then your solution would work but is too much code (and overhead) for just providing a nice error message. And as I said – no offense meant – maybe there is no solution as short as I wish for to the problem I am facing here :slight_smile:


#4

I don’t see anything resembling an offense in your words. You are at the right place to ask such questions (very specific questions are fine, as long as they don’t demand immediate answers) and you totally have the right to not accept some answer for your particular needs.

From what I understand from your description, you cannot avoid stating the valid “couples” in one way or the other. The question is what approach would be better in your case. I merely stated an obvious one.

If the amount of code is a concern (which is a reasonable concern), you may want to explore a solution with metaprogramming, which is advanced stuff but very useful in such cases, and Julia is quite powerful in that field.

There is also a good chance that there is some package in the Julia ecosystem which deals with situations like yours (other people who faced that problem may have provided a general solution in the form of a package).

Another approach is to provide the “coupling” info as a parameter of your types (code posted here).

I can think of a couple of other approaches, but they are trade-offs on what you consider more important in your case.


#5

In that scenario I don’t want to add two lines for every quiteGeneral function just to kind of enable the certain combination.

You could meta-program that away, something like this:

for sym in (:B, :C)
    p, q = Symbol(sym, 1), Symbol(sym, 2)
    @eval quiteGeneral(p::$p, q::Vector{$q}) = do_quiteGeneral(p, q)
end

#6

I will definetly look into meta programming, I’m quite new to Julia but I really kind a like it.

Of course you have to state the statement of a valid couple. My hope was to state the valid couple only once, something like

type Couple(B1,B2) <: Couple(A1,A2)

(of course thats not correct Julia code, just the idea) and then define functions like

quiteGeneral{ (T1,T2) <: Couple(A1,A2)}(p::T1, q::Vector{T2})

or even introduce some helping Type A coupling them, but I haven’t seen the possibility how to do that. And maybe the whole Idea is also a little too overtyped.


#7

You can use traits for this:

abstract Match
abstract NotMatch
"If the two arguments match, i.e. they can work together, then it returns Match otherwise NotMatch."
function ismatch end
ismatch(::Type{B1}, ::Type{B2}) = Match
ismatch(::Type{C1}, ::Type{C2}) = Match
ismatch(::Any, ::Any) = NotMatch # catch-all defaults to NotMatch

# Now write your function like so:
quite_general{T<:A1, S<:A2}(p::T, q::Vector{S})::T = _quite_general(ismatch(T,S), p,q) # trait-dispatch
_quite_general(::Type{Match},p,q) = ... # some logic
_quite_general{T<:A1, S<:A2}(::Type{NotMatch}, p::T, q::Vector{S}) = error("Type $T and $S don't match")

So for each type combination you have to specify whether they match or not with ismatch (but just this once). Then each function which does something according to the match, you have to split up into the trait-dispatch and the logic function. There is a new manual section about this: https://github.com/JuliaLang/julia/blob/9bad705a41ab68b54b0ded18eb3c386e187ad45a/doc/manual/methods.rst#4-trait-based-dispatch (not merged yet thus not well rendered) and there is the package SimpleTraits.jl which provides macro-sugar for above pattern.


Is there a Julian way to combine types generically or a design pattern that achieves the goal?
#8

Here is the promised code for the parameter approach.

First the “coupling” interface:

abstract A{L, N} # could also be an intermediate subtype of A

f{L}(p::A{L, 1}, q::A{L, 2})::A{L, 1} = # calculation goes here

Function purposely named f, cause it’s not the one in the question. But can be used if q is not a Vector. We are going to add support for Vectors soon.

Next an example implementation, e.g. for B types:

abstract B # could avoid that by using directly a symbol, e.g. :B

type B1 <: A{B, 1}
# fields
end

type B2 <: A{B, 2}
# fields
end

We can use current implementation like: f(B1(args), B2(args))

Now for Vector support. As pointed out in the manual:

julia>        B2  <:        A{B, 2}  # by definition
true

julia> Vector{B2} <: Vector{A{B, 2}} # invariant
false

So something like f{L}(p::A{L, 1}, q::Vector{A{L, 2}}) won’t work. To overcome that, we can add an abstract type to the interface and use it in the general function:

abstract VectorOfA{L, N}

quiteGeneral{L}(p::A{L, 1}, q::VectorOfA{L, 2})::A{L, 1} = # calculation with q.v goes here

Then update the example implementation with a simple Vector wrapper:

immutable VectorOfB2 <: VectorOfA{B, 2}
  v::Vector{B2}
end

Now we can make calls like quiteGeneral(B1(args), VectorOfB2([B2(args), B2(args), ...])).

That covers the question to the best of my understanding. So if that’s not enough, I will need extra information about the needs.


#9

While I like Traits and will try them, your approach also looks nice, though I think, for reading purposes, I would also do abstract A1 <: A{L,1} and abstract A2 <: A{L,2} for supertypes of the B1,B2 types…despite that little change…very nice!


#10

My code is only a proof of concept, not the only nor necessarily the best way to do the job. A lot depends on the rest of your code and design choices. For reading purposes, you may prefer using instead a type alias:

typealias A1{L} A{L, 1}

#11

Yes, that#s what I saw it for and my idea was the first to improve the proof of concept, but indeed a typealias is better :slight_smile: