F#从CSV文件

时间:2017-09-04 10:41:32

标签: f# tree

我正在努力学习F#,我真的很喜欢到目前为止所看到的。 我正在尝试将一些C#代码实现为F#思维方式,作为练习和学习的练习。

如果之前已经回答过,我真的很抱歉,但我无法找到解决所有问题的答案。

我们拥有销售队伍结构,我们有销售主管和普通销售人员。主管可能有也可能没有主管。

所有销售数据均来自CSV格式的其他系统。 在阅读记录时,我们不知道SalesPerson是否有报告。

我似乎并不了解如何在F#的不可变世界中加载树。我相信有办法。

我们简化的遗留C#代码定义(翻译成Enligsh)

public class SalesPerson
{
    public int Id { get; set; }
    public SalesPerson Supervisor { get; set; }
    public List<SalesPerson> Reports { get; private set; } = new List<SalesPerson>();
    public PersonalSales double { get; set; }
    public GroupSales double { get; set; }
}

这是代码的过度简化版本。但是,问题仍然存在:如何加载树?

我提出了以下F#类型

type SalesPerson = {
    Id : int
    Supervisor : SalesPerson option
    Reports : List<SalesPerson> option
    PersonalSales : double
    GroupSales : double
}

我甚至不确定这是否是定义类型的F#方式。

我的问题是:

  1. 主管指向另一个SalesPerson,它是不可变的。如果它被新的替换(因为不可变数据有效),引用将会中断。
  2. 报告是不可变的。我认为我可以使用C#&#39; List<T>,但我不确定这是否是F#方式。
  3. 主管的报告记录不遵循主管记录。它们可能会在下面出现X行,而不是所有行。但是,系统会确保Supervisor记录始终位于该主管的任何“报告”记录之前。
  4. 如何在加载树后更新GroupSales计算字段。
  5. 示例CSV文件如下所示:

    1,,100.00
    2,,110.00
    3,1,50.00
    4,1,75.00
    5,2,80.00
    6,,92.00
    

    所以:

    1 -> 2 reports
    2 -> 1 report
    3,4,5,6 -> No reports
    

    我真的很感激任何&#34;光&#34;你可能会对这些问题大放异彩。

    ...谢谢

2 个答案:

答案 0 :(得分:4)

如果将树结构分离为单独的类型,这会变得容易一些。不可变树的通常方法是这样的:

let rawData =
    [ 1, None, 100.00
      2, None, 110.00
      3, Some 1, 50.00
      4, Some 1, 75.00
      5, Some 2, 80.00
      6, None, 92.00 ]

let dataMap = rawData |> List.groupBy (fun (_, superId, _) -> superId) |> Map
let getChildrenData personId = dataMap |> Map.tryFind personId |> Option.defaultValue []

type Tree<'a> = { Data: 'a; Children : List<Tree<'a>> }

type SalesPerson = { Id : int; SupervisorId : int option; PersonalSales : double; GroupSales : double }

let salesPersonTree =
    let rec buildNode (id, superId, sales) =
        let children = getChildrenData (Some id) |> List.map buildNode
        let groupSales = (children |> List.sumBy (fun x -> x.Data.GroupSales)) + sales
        { Data = { Id = id; SupervisorId = superId; PersonalSales = sales; GroupSales = groupSales }
          Children = children }

    let topLevelItems = getChildrenData None
    topLevelItems |> List.map buildNode

总结:通过父对数据进行分组,然后使用递归函数从顶部节点(没有父节点的节点)开始构建树。因为我们构建了所有后代节点,所以我们完成了任何给定节点的构建,我们可以使用后代数据来计算GroupSales

您无法直接从给定节点访问父级,但您确实拥有父级ID。只要保留原始salesPeople列表,就可以获得任何给定父ID的完整数据。

拥有通用树类型的一个好处是,您可以在任何树上使用可重复使用的函数(例如map,fold,tryFind)。

答案 1 :(得分:2)

@TheQuickBrownFox在为您的域建模方面做得很好。

type Employee = { Id : int; SupervisorId : int option; PersonalSales : double }

使用记录/类来表示Tree是处理事物的OO方式, 当你没有很多FP经验时,这可能更容易掌握。

我想告诉你a more functional approach

type 'a Tree =
  | Leaf of 'a
  | Branch of 'a * 'a Tree list

Leaf个节点是层次结尾的SalesPerson个。 Supervisor及其所有爪牙都由Branch es表示并一直向上。

type SalesMember =
  | SalesPerson of Employee
  | Supervisor of Employee * SalesMember List

Tree也有一个根节点 - 只能有一个 - 您可以轻松编写一个函数来将rawData转换为:

let rawData =
    [ 0, None, 0.0
      1, Some 0, 100.00
      2, Some 0, 110.00
      3, Some 1, 50.00
      4, Some 1, 75.00
      5, Some 2, 80.00
      6, Some 0, 92.00 ]

let flatList =
    rawData
    |> List.map (fun (id, superId, sales) -> 
                     {Id = id; SupervisorId = superId; PersonalSales = sales})

let getTree salesPeople =
    // To do : validate root
    let root = salesPeople |> List.find (fun p -> p.SupervisorId = None)
    let children supervisorId = 
        salesPeople |> List.filter (fun p -> p.SupervisorId = Some supervisorId)

    let rec loop employee = 
      match children employee.Id with
      | [] -> SalesPerson employee
      | list -> Supervisor (employee, List.map loop list)

    loop root

let salesForce = getTree flatList 

要实施GroupSales,您可以展开Supervisor

type SalesMember =
  | SalesPerson of emp : Employee
  | Supervisor of emp : Employee * reports : List<SalesMember> * groupSales : double

构建此树实例的一种方法是从getTree函数转换树。处理,转换和优化树木是一个广泛的主题,因为for fun and profit始终是开始旅程的好地方。

更新 - 群组销售

为了简单起见,我只使用一个 Discriminated Union ,在第一次运行时将GroupSales设置为零。但是,您可以轻松地调整代码以转换为另一种类型的Tree

type Employee = { Id : int; SupervisorId : int option; PersonalSales : double }

type GroupSales = double
type SalesMember =
  | SalesPerson of Employee
  | Supervisor of Employee * SalesMember List * GroupSales

let rawData =
    [ 0, None, 0.
      1, Some 0, 100.00
      2, Some 0, 110.00
      3, Some 1, 50.00
      4, Some 1, 75.00
      5, Some 2, 80.00
      6, Some 0, 92.00 ]

let flatList =
    rawData
    |> List.map (fun (id, superId, sales) -> 
                     {Id = id; SupervisorId = superId; PersonalSales = sales})

let getTree salesPeople =
    let root = salesPeople |> List.find (fun p -> p.SupervisorId = None)
    let children supervisorId = 
        salesPeople |> List.filter (fun p -> p.SupervisorId = Some supervisorId)

    let rec loop employee = 
        match children employee.Id with
          | [] -> SalesPerson employee
          | list -> Supervisor (employee, List.map loop list, 0.)

    loop root

let transformTree root =
    let rec getGroupSales = function
      | SalesPerson emp 
        -> emp.PersonalSales
      | Supervisor (sup, reports, _) 
        -> sup.PersonalSales + List.sumBy getGroupSales reports

    let rec loop = function
      | Supervisor (sup, reports, _) as mem
        -> Supervisor (sup, List.map loop reports, getGroupSales mem)
      | salesPerson -> salesPerson

    loop root

let salesForce = 
    flatList 
    |> getTree
    |> transformTree

不太天真的实现会转换/计算GroupSales bottom-up instead of top-down,允许您使用已计算的GroupSales