由于循环引用,无法确定如何对 F# 类型进行排序的问题

14
我有一些类型,它们都扩展了一个公共类型,并且这些类型是我的模型。
之后,我为每种模型类型都有相应的DAO类型,用于CRUD操作。
现在我需要一个函数,可以根据任何模型类型找到id,因此我创建了一个新类型来存放一些杂项函数。
问题在于我不知道如何对这些类型进行排序。目前,我的模型排在DAO之前,但我需要让 `DAOMisc` 排在 `CityDAO` 之前,而 `CityDAO` 又要排在 `DAOMisc` 之前,这是不可能的。
简单的方法是将此函数放在每个DAO中,仅涉及可以在其之前出现的类型,例如 `State` 比 `City` 先出现,因为 `State` 与 `City` 存在外键关系,所以杂项函数会非常简短。但是,这似乎不太合适,因此我不确定如何最好地解决这个问题。
以下是我的杂项类型,其中 `BaseType` 是我所有模型的公共类型。
type DAOMisc =
    member internal self.FindIdByType item = 
        match(item:BaseType) with
        | :? StateType as i -> 
            let a = (StateDAO()).Retrieve i
            a.Head.Id
        | :? CityType as i -> 
            let a = (CityDAO()).Retrieve i
            a.Head.Id
        | _ -> -1

这里有一个数据访问对象(dao)类型。CommonDAO实际上包含了增删改查操作的代码,但这并不重要。

type CityDAO() =
    inherit CommonDAO<CityType>("city", ["name"; "state_id"], 
        (fun(reader) ->
            [
                while reader.Read() do
                    let s = new CityType()
                    s.Id <- reader.GetInt32 0
                    s.Name <- reader.GetString 1
                    s.StateName <- reader.GetString 3
            ]), list.Empty
    )

这是我的模型类型:

type CityType() =
    inherit BaseType()
    let mutable name = ""
    let mutable stateName = ""
    member this.Name with get() = name and set restnameval=name <- restnameval
    member this.StateName with get() = stateName and set stateidval=stateName <- stateidval
    override this.ToSqlValuesList = [this.Name;]
    override this.ToFKValuesList = [StateType(Name=this.StateName);]
这个FindIdByType函数的目的是为了寻找外键关系的id,这样我就可以在我的模型中设置值,然后让CRUD函数使用所有正确的信息执行操作。因此,City需要州名称的id,所以我会获取州名称,将其放入state类型中,然后调用此函数获取该州的id,这样我的城市插入也将包括外键的id。
这似乎是处理插入的最佳方法,非常通用,这正是我试图解决的当前问题。
更新:
我需要研究一下,看看是否可以在定义所有其他DAO之后将FindIdByType方法注入CommonDAO中,几乎就像是闭包一样。如果这是Java,我会使用AOP来获得我正在寻找的功能,但不确定如何在F#中实现。
最终更新:
在考虑我的方法后,我意识到它有致命的缺陷,因此我想出了另一种方法。
这就是我将进行插入的方式,并且我决定将这个想法放入每个实体类中,这可能是一个更好的想法。
member self.Insert(user:CityType) =
    let fk1 = [(StateDAO().Retrieve ((user.ToFKValuesList.Head :?> StateType), list.Empty)).Head.Id]
    self.Insert (user, fk1)

我还没有开始使用fklist,但它是一个int list,我知道每个列名对应哪个列表,所以我只需要为选择执行inner join。例如:

这是通用的基本类型插入:

member self.Insert(user:'a, fklist) =
    self.ExecNonQuery (self.BuildUserInsertQuery user)

如果F#可以做协变/逆变就好了,所以我不得不绕过这个限制。


请参见https://dev59.com/qXM_5IYBdhLWcg3wcCrc。 - Brian
@Brian - 直到我思考了另一个问题,我才没有考虑只是在几种类型之间使用“and”。在我看来,这似乎是对我自己设计缺陷的一种hack方法,但我认为这是最好的方法。 - James Black
4个回答

11

在 F# 中,可以定义 互相递归类型,也就是说,你可以同时定义需要相互引用的两种类型,并且它们将能够看到对方。编写这种类型的语法如下:

type CityDAO() = 
  inherit CommonDAO<CityType>(...)
  // we can use DAOMisc here

and DAOMisc = 
  member internal self.FindIdByType item =  
    // we can use CityDAO here
这种语法的限制在于两种类型需要在单个文件中声明,因此您无法使用典型的C#组织方式每个文件1种类型。正如诺曼所指出的那样,这不是典型的函数式设计,因此如果您以更函数式的方式设计整个数据访问层,则可能可以避免此问题。但是,我认为在F#中将函数式和面向对象的风格结合起来没有问题,因此使用相互递归类型可能是唯一的选择。如果您首先为这两种类型定义接口,那么您可能可以更好地编写代码-这些接口可能需要相互递归(这取决于一种类型是否在另一种类型的公共接口中使用):
type ICityDAO = 
  abstract Foo : // ...

type IDAOMisc = 
  abstract Foo : // ...

这样做有以下好处:

  • 在单个文件中定义所有相互递归的接口不会使代码 less readable
  • 您稍后可以引用这些接口,因此不需要其他类型相互递归
  • 作为副作用,您将拥有更可扩展的代码(感谢接口)

我可能会使用一个接口,但是我对这个解决方案并不满意,因为它似乎有些笨拙。我认为我的设计中存在一些根本性的问题。我在问题上添加了一个编辑关于这个问题。 - James Black

8

这个例子与我在函数式编程中习惯的风格截然不同。但是对于相互递归类型排序的问题,有一个标准解决方案:使用类型参数并创建两级类型。我将在OCaml中给出一个简单的示例,这是一种相关的语言。我不知道如何将简单的示例翻译成您正在使用的可怕类型函数。

以下内容无法正常工作:

type misc = State of string
          | City  of city

type city = { zipcode : int; location : misc }

这里是使用两级类型修复的方法:

请注意以下步骤:

type 'm city' = { zipcode : int; location : 'm }

type misc = State of string
          | City of misc city'
type city = misc city'

这个例子是OCaml语言的,但也可以推广到F#语言中。希望这能有所帮助。


1
我需要好好思考一下你展示的内容,看起来这可能比我现在做的更好。 - James Black
经过思考你所写的内容,我意识到你是正确的,我的方法是错误的。 - James Black
我也需要花一些时间思考这个好的解决方案 :) - Mário Meyrelles

5
F#直接支持相互递归类型。考虑以下鸡/蛋类型定义:
type Chicken =
   | Eggs of Egg list
and Egg =
   | Chickens of Chicken list

重点是互相递归的类型使用 'and' 运算符一起声明(而不是两个独立的类型)

我相信我曾尝试过你的方法,但当我写下我的问题时,它无法正常工作。F# 3.0可能已经使这成为可能,但我目前还没有尝试过。 - James Black

2
如何消除DAOMisc.FindIdByType,并用每个DAO类内的FindId替换它?FindId只知道如何查找自己的类型。这将消除基类和动态类型测试的需要,以及DAOMisc和所有其他DAO类之间的循环依赖关系。 DAO类型可以相互依赖,因此CityDAO可以调用StateDAO.FindId。(如果需要,DAO类型可以相互依赖。)
当你说“简单的方法是将此函数放在每个DAO中...但是,这让我感到不对劲...”时,这是否就是你所说的内容?我不确定,因为你说该函数仅涉及到它之前的类型。我在这里提出的想法是每个FindId函数只知道自己的类型。

我正在努力尽量减少重复的代码,并使我的函数尽可能通用,这就是为什么在每个DAO类中重复函数将是一个问题的原因。 - James Black
1
我采用了你的想法。 - James Black

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接