从函数返回可区分联合的替代方案是什么?

What are the alternatives to returning a discriminated union from a function?

我正在尝试使用 F# 重写一段复杂的代码。 对于这个特定的代码库,discriminated union 帮了我很多,所以我专注于尽可能多地使用它们。具体来说,对 DU 的详尽检查帮助我避免了很多错误。

但是,我面临着不得不使用 match ... with 的重复模式,以至于代码中的混乱抵消了我从详尽检查中获得的好处。 我尽可能地简化了我正在处理的模式,并试图提出一个示例来演示我正在编写的代码的结构。真正的代码库要复杂得多,它在一个完全不同的领域,但在语言层面,这个例子代表了问题。

假设我们想根据购物者的分类获得一些关于购物者的信息:他们要么是猫人,要么是狗人。这里的关键是通过 DU 对某些类型(元组)进行分类。 以下是域类型:

type PetPerson =
    |CatPerson
    |DogPerson

type CatFood =
    |Chicken
    |Fish

type DogFood =
    |Burger
    |Steak

//some cat food, shopper's age and address
type CatFoodShopper = CatFoodShopper of (CatFood list * int * string)    

//some dog food, shopper's age and number of children
type DogFoodShopper = DogFoodShopper of (DogFood list * int * int)

撇开我们喂养可怜的动物的可怕方式不谈,这个领域模型需要一个函数来将 PetPerson 映射到 CatFoodShopperDogFoodShopper
在这一点上,我最初的想法是定义一个 Shopper 类型,因为我不能根据模式匹配的结果 return 来自以下函数的两种不同类型:

type Shopper =
    |CatFShopper of CatFoodShopper
    |DogFShopper of DogFoodShopper

let ShopperViaPersonality = function
    |CatPerson -> CatFShopper (CatFoodShopper ([Chicken;Fish], 32, "Hope St"))
    |DogPerson -> DogFShopper (DogFoodShopper ([Burger;Steak], 45, 1))

这解决了问题,但我在代码中有很多地方(真的很多),我最终得到一个 PetPerson 并且需要得到一个 CatFoodShopperDogFoodShopper 基于 PetPerson 的值。对于我知道我手头没有的情况,这会导致不必要的模式匹配。这是一个例子:

let UsePersonality (x:int) (y:PetPerson) =
    //x is used in some way etc. etc.
    match y with
    |CatPerson as c -> //how can I void the following match?
        match (ShopperViaPersonality c) with
        |CatFShopper (CatFoodShopper (lst,_,_))-> "use lst and return some string "
        | _ -> failwith "should not have anything but CatFShopper"
    |DogPerson as d -> //same as before. I know I'll get back DogFShopper
        match (ShopperViaPersonality d) with
        |DogFShopper (DogFoodShopper (lst, _,_)) -> "use lst and return other string"
        |_ -> failwith "should not have anything but DogFShopper"

如您所见,我必须编写模式匹配代码,即使我知道我将取回特定值。我无法简明地将 CatPerson 值与 CatFoodShopper 值相关联。

为了改进调用站点的内容,我考虑使用 F# 通过接口模仿类型的方式 类,基于此处提供的大量示例:

type IShopperViaPersonality<'T> =
    abstract member ShopperOf: PetPerson -> 'T

let mappingInstanceOf<'T> (inst:IShopperViaPersonality<'T>) p = inst.ShopperOf p

let CatPersonShopper =
    {new IShopperViaPersonality<_> with
        member this.ShopperOf x =
            match x with
            |CatPerson -> CatFoodShopper ([Chicken;Fish], 32, "Hope St")
            | _ -> failwith "This implementation is only for CatPerson"}
let CatPersonToShopper = mappingInstanceOf CatPersonShopper

let DogPersonShopper =
    {new IShopperViaPersonality<_> with
        member this.ShopperOf x =
            match x with
            |DogPerson -> DogFoodShopper ([Burger;Steak], 45, 1)
            | _ -> failwith "This implementation is only for DogPerson"}
let DogPersonToShopper = mappingInstanceOf DogPersonShopper    

所以我不再使用 Shopper 类型来表示猫食购物者和狗食购物者,而是一个接口定义了从 PetPerson 值到特定购物者类型的映射。我也有单独的部分应用功能,使呼叫站点的事情变得更加容易。

let UsePersonality1 (x:int) (y:PetPerson) =
    match y with
    |CatPerson as c ->
        let (CatFoodShopper (lst,_,_)) = CatPersonToShopper c
        "use lst and return string"
    |DogPerson as d ->
        let (DogFoodShopper (lst,_,_)) = DogPersonToShopper d
        "use lst and return string"

这种方法在使用 PetPerson 值时效果更好,但我现在的任务是定义这些单独的函数以保持调用站点的整洁。

请注意,此示例旨在演示使用 DU 和使用基于分类 DU 参数的 return 不同类型的接口之间的权衡,如果我可以这样称呼的话。所以不要挂断我对 return 值等毫无意义的使用

我的问题是:有没有其他方法可以完成对一堆元组(或记录)类型进行分类的语义?如果您正在考虑活动模式,则它们不是一个选项,因为在实际代码库中,DU 有超过七个案例,这是活动模式的限制,以防它们有所帮助。那么我还有其他选择可以改进上述方法吗?

解决此问题的一个明显方法是在 PetPerson 匹配 PetPerson 之前调用 ShopperViaPersonality ,而不是在

之后调用
let UsePersonality (x:int) (y:PetPerson) =
    //x is used in some way etc. etc.
    match ShopperViaPersonality y with
    | CatFShopper (CatFoodShopper (lst,_,_))-> "use lst and return some string "
    | DogFShopper (DogFoodShopper (lst, _,_)) -> "use lst and return other string"

另请注意,如果 ShooperViaPersonality 的唯一目的是支持模式匹配,则最好将其设为活动模式:

let (|CatFShopper|DogFShopper|) = function
    | CatPerson -> CatFShopper ([Chicken;Fish], 32, "Hope St")
    | DogPerson -> DogFShopper ([Burger;Steak], 45, 1)

那么你可以这样使用它:

let UsePersonality (x:int) (y:PetPerson) =
    //x is used in some way etc. etc.
    match y with
    | CatFShopper (lst,_,_) -> "use lst and return some string "
    | DogFShopper (lst, _,_) -> "use lst and return other string"

从逻辑上讲,活动模式与 DU + 函数几乎相同,但在语法级别上,请注意现在嵌套少了多少。