Parametric Types - am I using correctly

Wanted to check I’m using parametric types as intended.
Two examples below comparing parametric types to using @match macro.
I think parametric types are better but the macro does isolate the core logic nicely
eg.
:linear => a + x*(b-a)
:geo => a*(b/a)^x

It would be nice to restrict the parameter values
e.g. struct interp_2{interp_type<:Interp_Type}
this would give an error in the below examples
Is there a better method?
Interested in any comments.

######  COMPARISON 1 #############
@enum Interp_Type linear geo


#######    With Parametric Types   ########3
struct interp_2{interp_type}
    a::Float64
    b::Float64
end

((;a,b)::interp_2{linear})(x) = a + x*(b-a)
((;a,b)::interp_2{  geo })(x) = a*(b/a)^x

interp_2{linear}(1,2)(.5) #1.5
interp_2{ geo  }(1,2)(.5) #1.41421


########## With @match macro   ##############
struct interp_2_2
    a::Float64
    b::Float64
    interp_type::Interp_Type
end

((;a,b,interp_type)::interp_2_2)(x) = @match Symbol(interp_type) begin
    :linear =>  a + x*(b-a) 
    :geo    =>  a*(b/a)^x
end

interp_2_2(1,2,linear)(.5) #1.5
interp_2_2(1,2,geo   )(.5) #1.41421



######## COMPARISON 2 #####################
@enum Day_Count ACT_365 ACT_360


###### with Parametric Type #############
struct coupon{day_count}
    days::Int64
end

Amount(c::coupon{ACT_365}) = c.days / 365
Amount(c::coupon{ACT_360}) = c.days / 360

Amount(coupon{ACT_360}(90)) #.25
Amount(coupon{ACT_365}(90)) #.2465


############# With @match macro ################
struct coupon_2
    day_count::Day_Count
    days::Int64
end

Amount(c::coupon_2) = @match Symbol(c.day_count) begin
    :ACT_360 => c.days / 360
    :ACT_365 => c.days / 365    
end

Amount(coupon_2(ACT_360,90)) #.25
Amount(coupon_2(ACT_365,90)) #.2465
1 Like

Just a minor note: Types are customarily typed in in “hammer” case. Coupon2, for instance, or SpiffyTypeWithNoDiscerniblePurpose. Makes it easier for readers of the code to make sense of it.

1 Like

I don’t see anything wrong per se. You can also use a type parameter instead of a field in the @match version, they’re not mutually exclusive.

julia> @enum Interp_Type linear geo

julia> struct interp_2_3{interp_type}
           a::Float64
           b::Float64
       end

julia> using Match

julia> ((;a,b)::interp_2_3{T})(x) where T = @match Symbol(T) begin
           :linear =>  a + x*(b-a) 
           :geo    =>  a*(b/a)^x
       end

julia> interp_2_3{linear}(1,2)(.5)
1.5

julia> interp_2_3{ geo  }(1,2)(.5)
1.4142135623730951

The question is, do you need methods to be compiled separately for linear vs geo flags. If so, you can branch at compile-time, preferably via multimethods but may also occur when the compiler optimizes a method call, at the cost of compiling more code per flag. If not, store the flag in a field and use @match or an if-statement to branch at runtime.

What are the pros and cons of branching at run time vs compile time?

Generally, the pros of compile-time branching is 1) don’t have to spend time branching at runtime, 2) restores type stability if the branches result in different types, 3) optimizations may go further if compiler can deduce only 1 branch runs. The con is having to compile a method more times, like for every concrete interp_2_3{interp_type}.

Whether a value should be a field or a type parameter depends on the situation. If a value takes arbitrarily many values, then it’s more worth to vary that per instance so you can compile a method once for all those values. If it only takes a few finite values and work separately (so you wouldn’t have an array mixing different interp_types), it could be beneficial to make a type parameter you compile a few times for. Taking it to extremes, it seems obvious to make billions of temperature::Float64 values vary per instance but ~5 N::Int values vary per Array{T, N} type.

1 Like

thanks for this :slight_smile: