2017-09-04 26 views
5

我想學習F#,我非常喜歡迄今爲止所看到的。 我試圖實現一些C#代碼F#的思維方式作爲練習和學習。F#從CSV文件中加載一棵樹

我真的很抱歉,如果它已被回答,但我找不到解決我所有問題的答案。

我們有一個銷售團隊結構,我們有銷售主管和普通銷售人員。主管可能會或可能沒有主管。

所有銷售數據來自另一個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. 主管分給其他銷售人員,這是不可改變的。如果它被替換爲新的(因爲不可變數據有效),則引用將會中斷。
  2. 報告是不可變的。我認爲我可以使用C#的List<T>,但我不確定這是否是F#的方式。
  3. 主管報告記錄不遵循主管記錄。他們可能會在下面排成X行,而不是全部在一起。但是,系統會確保主管記錄始終在該主管的任何報告記錄之前。
  4. 如何在樹加載後更新GroupSales計算字段。

樣本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 

我真的很感激任何 「光」,你可能會在這些問題上大放異彩。

謝謝...

回答

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)。

+0

太棒了....我開始看到你是如何構建結構的。不過,我確實有第4點,我不知道該怎麼做。你會怎麼做'GroupSales'? (自己的「個人銷售」+兒童的「個人銷售」)。我只想計算一次,而不是每次我們要求它。 TIA,David –

+0

@CrazySpaniard這是可能的,因爲我們在建立給定節點時有後代節點數據可用。但是,它的確意味着延遲創建記錄類型,直到後來我們可以計算'GroupSales'。這可能意味着更早的類型安全性(就像我上面所做的那樣,使用未標記的元組更長時間)或爲原始數據創建中間類型。 – TheQuickBrownFox

+0

@CrazySpaniard我假設你的意思是'GroupSales' ='PersonalSales' +孩子的'GroupSales',它相當於'PersonalSales' + *後代'*'PersonalSales'。 – TheQuickBrownFox

2

@TheQuickBrownFox在建模領域做得很好。

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

使用記錄/類來表示一個Tree是處理事情, 這可能是更容易掌握,當你沒有很多經驗FP一個面向對象的方式。我想告訴你a more functional approach

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

Leaf節點是SalesPerson s的層次結構的端部。 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是開始你的旅程的好地方。

更新 - GroupSales

爲了簡單起見,我會只用一個識別聯合,在第一次運行設置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 

一個不太懂事的實施將變換/鈣GroupSalesbottom-up instead of top-down,讓您的使用已經計算GroupSales

+0

我曾經使用Leaf/Branch DU這樣的樹(有一個子列表),但我想知道是否有任何需要。如果一個節點有一個空的子列表,那麼它就是一片葉子。它也打開了無效狀態的可能性:您可以創建一個帶有空列表的分支。我認爲當你有一定數量的孩子時,DU方法是有意義的,例如,一棵二叉樹。 – TheQuickBrownFox

+0

我不認爲使用DU而不是記錄是或多或少的功能。 – TheQuickBrownFox

+0

@TheQuickBrownFox我相信有一個區別,功能語言都是關於類型(和處理它們的函數)。在你的設計中,'SalesPerson'和'Supervisor'之間沒有區別。考慮到這一點,你會發現'groupSales'字段更適合於'Supervisor'級別,因爲他擁有該組。你如何實現這一點,而不用裝飾'SalesPerson'? – Funk