heterogeneous lists through flexible types

淺唱寂寞╮ 提交于 2019-12-04 14:29:43

问题


I am trying to stick heterogeneous types in a list making use of flexible types

type IFilter<'a> = 
    abstract member Filter: 'a -> 'a

type Cap<'a when 'a: comparison> (cap) = 
    interface IFilter<'a> with
        member this.Filter x = 
            if x < cap
            then x
            else cap

type Floor<'a when 'a: comparison> (floor) = 
     interface IFilter<'a> with
        member this.Filter x = 
            if x > floor
            then x
            else floor

type Calculator<'a, 'b when 'b:> IFilter<'a>> (aFilter: 'b, operation: 'a -> 'a) = 
    member this.Calculate x = 
        let y = x |> operation
        aFilter.Filter y

type TowerControl<'a> () = 
    let mutable calculationStack = List.empty
    member this.addCalculation (x: Calculator<'a, #IFilter<'a>> ) =
        let newList = x::calculationStack
        calculationStack <- newList

let floor10 = Floor<int> 10
let calc1 = Calculator<int, Floor<int>> (floor10, ((+) 10))

let cap10 = Cap 10
let calc2 = Calculator (cap10, ((-) 5))

let tower = TowerControl<int> ()
tower.addCalculation calc1
tower.addCalculation calc2

In the example above

member this.addCalculation (x: Calculator<'a, #IFiler<'a>> ) =  

produces the error

error FS0670: This code is not sufficiently generic. The type variable 'a could not be generalized because it would escape its scope.

Apologies if a similar question has already been posted. Thank you.


回答1:


There's no easy way to do this. It looks like you really want calculationStack to have type:

(∃('t:>IFilter<'a>).Calculator<'a, 't>) list

but F# doesn't provide existential types. You can use the "double-negation encoding" ∃'t.f<'t> = ∀'x.(∀'t.f<'t>->'x)->'x to come up with the following workaround:

// helper type representing ∀'t.Calculator<'t>->'x
type AnyCalc<'x,'a> = abstract Apply<'t when 't :> IFilter<'a>> : Calculator<'a,'t> -> 'x

// type representing ∃('t:>IFilter<'a>).Calculator<'a, 't>
type ExCalc<'a> = abstract Apply : AnyCalc<'x,'a> -> 'x

// packs a particular Calculator<'a,'t> into an ExCalc<'a>
let pack f = { new ExCalc<'a> with member this.Apply(i) = i.Apply f }

// all packing and unpacking hidden here
type TowerControl<'a> () = 
    let mutable calculationStack = List.empty

    // note: type inferred correctly!
    member this.addCalculation x =
        let newList = (pack x)::calculationStack
        calculationStack <- newList

    // added this to show how to unpack the calculations for application
    member this.SequenceCalculations (v:'a) =
        calculationStack |> List.fold (fun v i -> i.Apply { new AnyCalc<_,_> with member this.Apply c = c.Calculate v }) v

// the remaining code is untouched

let floor10 = Floor<int> 10
let calc1 = Calculator<int, Floor<int>> (floor10, ((+) 10))

let cap10 = Cap 10
let calc2 = Calculator (cap10, ((-) 5))

let tower = TowerControl<int> ()
tower.addCalculation calc1
tower.addCalculation calc2

This has the big advantage that it works without modifying the Calculator<_,_> type, and that the semantics are exactly what you want, but the following disadvantages:

  1. It's hard to follow if you're unfamiliar with this way of encoding existentials.
  2. Even if you are familiar, there's a lot of ugly boilerplate (the two helper types) since F# doesn't allow anonymous universal qualification either. That is, even given that F# doesn't directly support existential types, it would be much easier to read if you could write something like:

    type ExCalc<'a> = ∀'x.(∀('t:>IFilter<'a>).Calculator<'a,'t>->'x)->'x
    let pack (c:Calculator<'a,'t>) : ExCalc<'a> = fun f -> f c
    
    type TowerControl<'a>() =
        ...
        member this.SequenceCalcualtions (v:'a) =
            calculationStack |> List.fold (fun v i -> i (fun c -> c.Calculate v)) v
    

    But instead we've got to come up with names for both helper types and their single methods. This ends up making the code hard to follow, even for someone already familiar with the general technique.

On the off chance that you own the Calculator<_,_> class, there's a much simpler solution that might work (it may also depend on the signatures of the methods of the real Calcuator<,> class, if it's more complex than what you've presented here): introduce an ICalculator<'a> interface, have Calculator<_,_> implement that, and make calculationStack a list of values of that interface type. This will be much more straightforward and easier for people to understand, but is only possible if you own Calculator<_,_> (or if there's already an existing interface you can piggy back on). You can even make the interface private, so that only your code is aware of its existence. Here's how that would look:

type private ICalculator<'a> = abstract Calculate : 'a -> 'a

type Calculator<'a, 'b when 'b:> IFilter<'a>> (aFilter: 'b, operation: 'a -> 'a) = 
    member this.Calculate x = 
        let y = x |> operation
        aFilter.Filter y
    interface ICalculator<'a> with
        member this.Calculate x = this.Calculate x

type TowerControl<'a> () = 
    let mutable calculationStack = List.empty
    member this.addCalculation (x: Calculator<'a, #IFilter<'a>> ) =
        let newList = (x :> ICalculator<'a>)::calculationStack
        calculationStack <- newList

    member this.SequenceCalculations (v:'a) =
        calculationStack |> List.fold (fun v c -> c.Calculate v) v


来源:https://stackoverflow.com/questions/21992466/heterogeneous-lists-through-flexible-types

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!