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
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.
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.
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.