【问题标题】:Functional way to add to Lists that are Class-Members添加到类成员列表的功能方法
【发布时间】:2015-11-25 06:47:47
【问题描述】:

我想对一个类的项目进行排序并将它们收集到 Collection-Classes 中,List-Member 旁边还包含排序过程所需的更多信息。

以下示例是针对我的问题的一个非常简化的示例。虽然没有意义,但我希望它仍然可以帮助理解我的问题。

type ItemType = Odd|Even  //realworld: more than two types possible

type Item(number) =
    member this.number = number
    member this.Type = if (this.number % 2) = 0 then Even else Odd

type NumberTypeCollection(numberType:ItemType , ?items:List<Item>) =
    member this.ItemType =  numberType
    member val items:List<Item> = defaultArg items List.empty<Item> with get,set
    member this.append(item:Item) =  this.items <- item::this.items

let addToCollection (collections:List<NumberTypeCollection>) (item:Item) =
    let possibleItem =
        collections
        |> Seq.where (fun c -> c.ItemType = item.Type) //in my realworld code, several groups may be returned
        |> Seq.tryFind(fun _ -> true) 

    match possibleItem with
        |Some(f) -> f.append item
                    collections
        |None   -> NumberTypeCollection(item.Type, [item]) :: collections

let rec findTypes (collections:List<NumberTypeCollection>) (items:List<Item>) =
    match items with
    | [] -> collections
    | h::t -> let newCollections = ( h|> addToCollection collections)
                findTypes newCollections t


let items = [Item(1);Item(2);Item(3);Item(4)]

let finalCollections = findTypes List.empty<NumberTypeCollection> items

我对 addToCollection 方法不满意,因为它要求 NumberTypeCollection 中的项目是相互的。也许还有其他问题。

什么是解决此问题的适当功能解决方案?

编辑:对不起。可能代码过于简化。这是一个稍微复杂一点的例子,希望能说明我为什么选择共同类成员(尽管这仍然可能是错误的决定):

open System

type Origin = Afrika|Asia|Australia|Europa|NorthAmerika|SouthAmerica

type Person(income, taxrate, origin:Origin) =
    member this.income = income
    member this.taxrate = taxrate
    member this.origin = origin


type PersonGroup(origin:Origin , ?persons:List<Person>) =
    member this.origin = origin
    member val persons:List<Person> = defaultArg persons List.empty<Person> with get,set
    member this.append(person:Person) =  this.persons <- person::this.persons

//just some calculations to group people into some subgroups
let isInGroup (person:Person) (personGroup:PersonGroup) =
    let avgIncome =
        personGroup.persons 
        |> Seq.map (fun p -> float(p.income * p.taxrate) / 100.0)
        |> Seq.average
    Math.Abs ( (avgIncome / float person.income) - 1.0 ) < 0.5


let addToGroup (personGroups:List<PersonGroup>) (person:Person) =
    let possibleItem =
        personGroups
        |> Seq.where (fun p -> p.origin = person.origin)
        |> Seq.where (isInGroup person)
        |> Seq.tryFind(fun _ -> true) 

    match possibleItem with
        |Some(f) -> f.append person
                    personGroups
        |None   -> PersonGroup(person.origin, [person]) :: personGroups

let rec findPersonGroups (persons:List<Person>) (personGroups:List<PersonGroup>) =
    match persons with
    | [] -> personGroups
    | h::t -> let newGroup = ( h|> addToGroup personGroups)
              findPersonGroups t newGroup


let persons = [Person(1000,20, Afrika);Person(1300,22,Afrika);Person(500,21,Afrika);Person(400,20,Afrika)]

let c = findPersonGroups persons List.empty<PersonGroup>

我可能需要强调一点:可以有几个不同的群体具有相同的起源。

【问题讨论】:

    标签: f# functional-programming


    【解决方案1】:

    如果您只想生成一次集合,Tomas 使用 groupby 的解决方案是最佳方法,它简单明了。

    如果您希望能够针对此类问题以功能性、引用透明的样式添加/删除项目,我建议您离开seq 并开始使用Map

    您有一个基本上类似于字典的设置。你有一个唯一的键和一个值。相当于字典的功能 F# 是 Map,它是基于 AVL 树的不可变数据结构。您可以在 O(log n) 时间内插入、删除和搜索。当您从Map 追加/删除时,旧的Map 会被保留,您会收到一个新的Map

    这是用这种风格表达的代码

    type ItemType = 
        |Odd
        |Even
    
    type Item (number) =
        member this.Number = number
        member this.Type = if (this.Number % 2) = 0 then Even else Odd
    
    type NumTypeCollection = {Items : Map<ItemType, Item list>}
    
    /// Functions on NumTypeCollection
    module NumberTypeCollection =
        /// Create empty collection
        let empty = {Items = Map.empty}
    
        /// Append one item to the collection
        let append (item : Item) numTypeCollection =
            let key = item.Type
            match Map.containsKey key numTypeCollection.Items with
            |true ->
                let value = numTypeCollection.Items |> Map.find key
                let newItems = 
                    numTypeCollection.Items
                    |> Map.remove key 
                    |> Map.add key (item :: value) // append item
                {Items = newItems }
            |false -> {Items = numTypeCollection.Items |> Map.add key [item]}
    
        /// Append a list of items to the collections
        let appendList (item : Item list) numTypeCollection =
            item |> List.fold (fun acc it -> append it acc) numTypeCollection
    

    然后调用它:

    let items = [Item(1);Item(2);Item(3);Item(4)]
    
    let finalCollections = NumberTypeCollection.appendList items (NumberTypeCollection.empty)
    

    【讨论】:

    • 对于我的最小示例来说,这是一个非常好的方法。可悲的是,我把它简化了。每个 ItemType 可以有几组列表。你的答案是正确的,但我的问题是错误的。bb
    • @HorstLemke 这超出了您几分钟前所做的编辑吗?我建议的代码似乎仍然适用于您提供的新示例,只需将ItemType 更改为Origin 并将Item 更改为Person
    • 在新示例中,应该可以根据每个组成员的收入创建多个非洲组。我的示例代码创建了三个具有相同 Origin Afrika 的人员组。第二组有 2 个成员,他们的收入差不多。..
    • 现在我明白了,也许吧。 PersGroupCollection 允许这样做。它是一个带有始终有一个条目的映射的序列。我可能需要一些时间才能得到这个。
    • @HorstLemke 啊,是的,我现在明白你的意思了。乍一看,很难发现这两个示例之间的区别。我认为您仍然可以使用这样的方法,但也许您需要额外级别的类型。例如,Maps 中的 Map
    【解决方案2】:

    如果我正确理解了您的问题,那么您正在尝试按类型对项目进行分组。最简单的方法是使用标准库函数Seq.groupBy。以下应实现与您的代码相同的逻辑:

    items
    |> Seq.groupBy (fun item -> item.Type)
    |> Seq.map (fun (key, values) ->
        NumberTypeCollection(key, List.ofSeq values))
    

    【讨论】:

      【解决方案3】:

      也许还有其他问题。

      大概吧。很难说,因为很难检测到 OP 代码的目的......仍然:

      为什么你甚至需要一个Item 类?相反,你可以简单地拥有一个itemType 函数:

      let itemType i = if i % 2 = 0 then Even else Odd
      

      这个函数是referentially transparent,这意味着如果你愿意,你可以用它的值替换它。这使它与属性 getter 方法一样好,但现在您已经避免了引入新类型。

      为什么要定义 NumberTypeCollection 类? 为什么不定义一个简单的记录?

      type NumberTypeList = { ItemType : ItemType; Numbers : int list }
      

      你可以像这样实现addToCollection

      let addToCollection collections i =
          let candidate =
              collections
              |> Seq.filter (fun c -> c.ItemType = (itemType i))
              |> Seq.tryHead
          match candidate with
          | Some x ->
              let x' = { x with Numbers = i :: x.Numbers }
              collections |> Seq.filter ((<>) x) |> Seq.append [x']
          | None ->
              collections |> Seq.append [{ ItemType = (itemType i); Numbers = [i] }]
      

      由于是不可变的,它不会改变输入 collections,而是返回一个新的 NumberTypeList 序列。

      还要注意使用Seq.tryHead 而不是Seq.tryFind(fun _ -&gt; true)

      不过,如果您尝试对项目进行分组,那么 Tomas 建议使用 Seq.groupBy 更合适。

      【讨论】:

        猜你喜欢
        • 2018-06-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-12-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多