I have an Effect hierarchy described by a class hierarchy in F#. For simplicity, the classes look as follows:
[<AbstractClass>]
type Effect<'a, 'b>() =
class end
and EffectA<'a, 'b>() =
inherit Effect<'a, 'b>()
and EffectB<'a, 'b>() =
inherit Effect<'a, 'b>()
The idea is that I will have a function to interpret such effects, where each effect will cause some kind of side effect in the system.
I would like to have a number of system threads, labeled worker threads, that each serve as an interpreter, i.e. each thread can interpret an effect indepedently of one another.
I would like to have a queue of such effects as shown above, indicating effects that are ready to be interpreted by the worker threads, such that the workers can fetch them and interpret them.
Now, my problem is, since these effects are generic on 'a and 'b, I am unable to have a single queue for all effects.
For example, this wouldn't work:
let add (eff : Effect<obj, obj>) (effs : Effect<obj, obj> list) =
eff :: effs
let test =
let effA = EffectA<int, string>()
let effB = EffectB<string, float>()
let effs = [effA]
let newEffs = add effB effs
()
To recap: I would like a way to have a queue (or any kind of collection) of Effects<obj, obj> (or any other types, as long as they can be in the collection together) where ALL effects could be kept, and then some way to retrieve them from the list, and then cast them back to Effect<'a, 'b> with their original types.
I have been looking into using flexible types, but have so far been unsuccessful in trying.
I hope it makes sense. Thanks.
EDIT: @JL0PD suggested to add a non-generic base class, which is something I've attempted. It does actually work. The test function works as expected.
My question, however, with this approach is that how do I keep track of the original types of each effect, such that I can cast them correctly?
[<AbstractClass>]
type Effect() = class end
type Effect<'a, 'b>(a : 'a, b : 'b) =
inherit Effect()
member _.A = a
member _.B = b
and EffectA<'a, 'b>(a : 'a, b : 'b) =
inherit Effect<'a, 'b>(a, b)
and EffectB<'a, 'b>(a : 'a, b : 'b) =
inherit Effect<'a, 'b>(a, b)
let add (eff : Effect) (effs : Effect list) =
eff :: effs
let test =
let effA = EffectA<int, string>(10, "abc")
let effB = EffectB<string, float>("xyz", 1.0)
let effs : Effect list = [effA]
let newEffs = add effB effs
let head = List.head newEffs :?> Effect<string, float>
let otherEff = List.head (List.rev newEffs) :?> Effect<int, string>
printfn $"head -> A: %A{head.A}, B: %A{head.B}"
printfn $"otherEff -> A: %A{otherEff.A}, B: %A{otherEff.B}"
()
I think that this might work. I am not sure if it is the most elegant solution, but it appears to do its job. If you know the types of generic parameters of the classes deriving from Effect, you can create a DU generic type that contains a union (set) of all types used across subtypes.
This way you can avoid reflection and unwieldy type checking in your match expressions.